UR-0.41000755023532023421 012121654175 11544 5ustar00abrummetgsc000000000000MANIFEST.SKIP000444023532023421 20412121654172 13471 0ustar00abrummetgsc000000000000UR-0.41^MYMETA.yml$ ^.git ^debian/ ^ubuntu-lucid/ ^alt/ ^dist-maint/ ^MANIFEST.bak$ ^_build/ ^Build$ \.tar\.gz$ ^blib ^i ^MYMETA\.json$ ^$ LICENSE000444023532023421 12126412121654173 12673 0ustar00abrummetgsc000000000000UR-0.41UR is licensed under the same terms as Perl itself, which means it is dually-licensed under either the Artistic or GPL licenses. Below are details of the Artistic License and, following it, the GPL. The "Artistic License" Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder as specified below. "Copyright Holder" is whoever is named in the copyright or copyrights for the package. "You" is you, if you're thinking about copying or distributing this Package. "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as uunet.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) give non-standard executables non-standard names, and clearly document the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. You may embed this Package's interpreter within an executable of yours (by linking); this shall be construed as a mere form of aggregation, provided that the complete Standard Version of the interpreter is so embedded. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whoever generated them, and may be sold commercially, and may be aggregated with this Package. If such scripts or library files are aggregated with this Package via the so-called "undump" or "unexec" methods of producing a binary executable image, then distribution of such an image shall neither be construed as a distribution of this Package nor shall it fall under the restrictions of Paragraphs 3 and 4, provided that you do not represent such an executable image as a Standard Version of this Package. 7. C subroutines (or comparably compiled subroutines in other languages) supplied by you and linked into this Package in order to emulate subroutines and variables of the language defined by this Package shall not be considered part of this Package, but are the equivalent of input as in Paragraph 6, provided these subroutines do not change the language in any way that would cause it to fail the regression tests for the language. 8. Aggregation of this Package with a commercial distribution is always permitted provided that the use of this Package is embedded; that is, when no overt attempt is made to make this Package's interfaces visible to the end user of the commercial distribution. Such use shall not be construed as a distribution of this Package. 9. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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 . 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 . Build.PL000444023532023421 640612121654174 13123 0ustar00abrummetgsc000000000000UR-0.41#!/usr/bin/env perl use warnings FATAL => 'all'; use strict; use Module::Build; my $subclass = Module::Build->subclass( class_name => 'UR::ModuleBuildSelf', code => q{ sub ACTION_docs { # ensure docs get man pages and html my $self = shift; $self->depends_on('code'); $self->depends_on('manpages', 'html'); } sub man1page_name { # without this we have "man ur-init.pod" instead of "man ur-init" my ($self, $file) = @_; $file =~ s/.pod$//; return $self->SUPER::man1page_name($file); } } ); my $build = $subclass->new( module_name => 'UR', license => 'perl', dist_author => [ 'Anthony Brummett brummett@cpan.org', 'Scott Smith sakoht@cpan.org', ], requires => { # known bugs with Perl 5.6 perl => 'v5.8.7', # pure Perl 'Class::Autouse' => '2.0', 'Class::AutoloadCAN' => '0.03', 'Clone::PP' => '1.02', 'Carp' => '', 'Sys::Hostname' => '1.11', 'File::Basename' => '2.73', 'File::Temp' => '', 'File::Path' => '', 'Lingua::EN::Inflect' => '1.88', 'Date::Format' => '', 'Data::Compare' => '0.13', 'Text::Diff' => '0.35', 'Path::Class' => '', #'Class::Inspector' => '', 'Text::Glob' => '', #'XML::Dumper' => '', #'XML::Generator' => '', #'XML::Simple' => '', 'version' => '', 'JSON' => '', 'Test::Fork' => '', 'Pod::Simple::Text' => '2.02', 'Pod::Simple::HTML' => '3.03', 'List::MoreUtils' => '', 'MRO::Compat' => '', # C 'FreezeThaw' => '0.43', 'YAML' => '', 'DBI' => '1.601', 'DBD::SQLite' => '1.14', 'Sub::Name' => '0.04', 'Sub::Install' => '0.924', 'Data::UUID' => '0.148', 'Devel::GlobalDestruction' => '', # possibly move to a web-specific #'Net::HTTPServer' => '', #'CGI::Application' => '', #'URI::Escape' => '', #'Getopt::Complete' => [ # we may migrate some of the Command logic here and really depend on it # currently it is actually not _required_ to function 'Getopt::Complete' => '0.26', #'XSLT' => [ # this stuff is hard to install and is only used by some views #'XML::LibXML' => '', #'XML::LibXSLT' => '', }, cpan_client => 'cpanm', script_files => [ 'bin/ur' ], test_files => [qw|t/*.t t/*/*.t t/*/*/*.t t/*/*/*/*.t|], bindoc_dirs => ['pod'], tap_harness_args => { 'jobs' => 8, 'rules' => { par => [ #{ seq => '../ext/DB_File/t/*' }, #{ seq => '../ext/IO_Compress_Zlib/t/*' }, #{ seq => '../lib/CPANPLUS/*' }, #{ seq => '../lib/ExtUtils/t/*' }, #'*' { seq => '../t/URT/t/42*' }, '*' ] }, }, ); foreach my $metadb_type ( qw(sqlite3 sqlite3n sqlite3-dump sqlite3n-dump sqlite3-schema sqlite3n-schema) ) { $build->add_build_element($metadb_type); } $build->create_build_script; INSTALL000444023532023421 7112121654174 12610 0ustar00abrummetgsc000000000000UR-0.41perl Build.PL ./Build ./Build test sudo ./Build install MANIFEST000444023532023421 5337712121654174 13011 0ustar00abrummetgsc000000000000UR-0.41bin/ur Build.PL Changes gmt-web/common.yml gmt-web/content/documentation.html gmt-web/content/index.html gmt-web/content/install.md gmt-web/content/res/images/icon_16.png gmt-web/content/res/images/icon_48.png INSTALL lib/above.pm lib/Command.pm lib/Command/Dispatch/Shell.pm lib/Command/DynamicSubCommands.pm lib/Command/Shell.pm lib/Command/SubCommandFactory.pm lib/Command/Test.pm lib/Command/Test/Echo.pm lib/Command/Test/Tree1.pm lib/Command/Test/Tree1/Echo1.pm lib/Command/Test/Tree1/Echo2.pm lib/Command/Tree.pm lib/Command/V1.pm lib/Command/V1.t lib/Command/V2.pm lib/Command/V2Deprecated.pm lib/Command/View/DocMethods.pm lib/Devel/callcount.pm lib/UR.pm lib/UR/All.pm lib/UR/BoolExpr.pm lib/UR/BoolExpr/BxParser.pm lib/UR/BoolExpr/BxParser.yp lib/UR/BoolExpr/Template.pm lib/UR/BoolExpr/Template/And.pm lib/UR/BoolExpr/Template/Composite.pm lib/UR/BoolExpr/Template/Or.pm lib/UR/BoolExpr/Template/PropertyComparison.pm lib/UR/BoolExpr/Template/PropertyComparison/Between.pm lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm lib/UR/BoolExpr/Template/PropertyComparison/False.pm lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm lib/UR/BoolExpr/Template/PropertyComparison/In.pm lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm lib/UR/BoolExpr/Template/PropertyComparison/Like.pm lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm lib/UR/BoolExpr/Template/PropertyComparison/NotEqual.pm lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm lib/UR/BoolExpr/Template/PropertyComparison/True.pm lib/UR/BoolExpr/Util.pm lib/UR/Change.pm lib/UR/Context.pm lib/UR/Context/DefaultRoot.pm lib/UR/Context/ImportIterator.pm lib/UR/Context/LoadingIterator.pm lib/UR/Context/ObjectFabricator.pm lib/UR/Context/Process.pm lib/UR/Context/Root.pm lib/UR/Context/Transaction.pm lib/UR/DataSource.pm lib/UR/DataSource.pod lib/UR/DataSource/Code.db lib/UR/DataSource/Code.pm lib/UR/DataSource/Code.schema lib/UR/DataSource/CSV.pm lib/UR/DataSource/Default.pm lib/UR/DataSource/File.pm lib/UR/DataSource/FileMux.pm lib/UR/DataSource/Filesystem.pm lib/UR/DataSource/Meta.pm lib/UR/DataSource/Meta.sqlite3 lib/UR/DataSource/Meta.sqlite3-bak lib/UR/DataSource/Meta.sqlite3-dump lib/UR/DataSource/Meta.sqlite3-dump-boostrap lib/UR/DataSource/Meta.sqlite3-schema lib/UR/DataSource/MySQL.pm lib/UR/DataSource/Oracle.pm lib/UR/DataSource/Pg.pm lib/UR/DataSource/QueryPlan.pm lib/UR/DataSource/RDBMS.pm lib/UR/DataSource/RDBMS/BitmapIndex.pm lib/UR/DataSource/RDBMS/Entity.pm lib/UR/DataSource/RDBMS/FkConstraint.pm lib/UR/DataSource/RDBMS/FkConstraintColumn.pm lib/UR/DataSource/RDBMS/PkConstraintColumn.pm lib/UR/DataSource/RDBMS/Table.pm lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm lib/UR/DataSource/RDBMS/TableColumn.pm lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm lib/UR/DataSource/SQLite.pm lib/UR/DataSource/ValueDomain.pm lib/UR/DBI.pm lib/UR/DBI/Report.pm lib/UR/Debug.pm lib/UR/DeletedRef.pm lib/UR/Doc/Pod2Html.pm lib/UR/Doc/Section.pm lib/UR/Doc/Writer.pm lib/UR/Doc/Writer/Html.pm lib/UR/Doc/Writer/Pod.pm lib/UR/Env.pod lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm lib/UR/Env/UR_CONTEXT_BASE.pm lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm lib/UR/Env/UR_CONTEXT_ROOT.pm lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm lib/UR/Env/UR_DBI_MONITOR_DML.pm lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm lib/UR/Env/UR_DBI_MONITOR_SQL.pm lib/UR/Env/UR_DBI_NO_COMMIT.pm lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm lib/UR/Env/UR_IGNORE.pm lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm lib/UR/Env/UR_NR_CPU.pm lib/UR/Env/UR_RUN_LONG_TESTS.pm lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm lib/UR/Env/UR_TEST_FILLDB.pm lib/UR/Env/UR_TEST_QUIET.pm lib/UR/Env/UR_USE_ANY.pm lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm lib/UR/Env/UR_USED_LIBS.pm lib/UR/Env/UR_USED_MODS.pm lib/UR/Exit.pm lib/UR/Manual.pod lib/UR/Manual/Cookbook.pod lib/UR/Manual/Metadata.pod lib/UR/Manual/Overview.pod lib/UR/Manual/Presentation.pod lib/UR/Manual/SchemaDesign.pod lib/UR/Manual/Tutorial.pod lib/UR/Manual/UR_Presentation.pdf lib/UR/ModuleBase.pm lib/UR/ModuleBuild.pm lib/UR/ModuleConfig.pm lib/UR/ModuleLoader.pm lib/UR/Namespace.pm lib/UR/Namespace/Command.pm lib/UR/Namespace/Command.pm.opts lib/UR/Namespace/Command/Base.pm lib/UR/Namespace/Command/Define.pm lib/UR/Namespace/Command/Define/Class.pm lib/UR/Namespace/Command/Define/Datasource.pm lib/UR/Namespace/Command/Define/Datasource/File.pm lib/UR/Namespace/Command/Define/Datasource/Mysql.pm lib/UR/Namespace/Command/Define/Datasource/Oracle.pm lib/UR/Namespace/Command/Define/Datasource/Pg.pm lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm lib/UR/Namespace/Command/Define/Db.pm lib/UR/Namespace/Command/Define/Namespace.pm lib/UR/Namespace/Command/Init.pm lib/UR/Namespace/Command/List.pm lib/UR/Namespace/Command/List/Classes.pm lib/UR/Namespace/Command/List/Modules.pm lib/UR/Namespace/Command/List/Objects.pm lib/UR/Namespace/Command/Old.pm lib/UR/Namespace/Command/Old/DiffRewrite.pm lib/UR/Namespace/Command/Old/DiffUpdate.pm lib/UR/Namespace/Command/Old/ExportDbicClasses.pm lib/UR/Namespace/Command/Old/Info.pm lib/UR/Namespace/Command/Old/Redescribe.pm lib/UR/Namespace/Command/RunsOnModulesInTree.pm lib/UR/Namespace/Command/Show.pm lib/UR/Namespace/Command/Show/Properties.pm lib/UR/Namespace/Command/Show/Schema.pm lib/UR/Namespace/Command/Show/Subclasses.pm lib/UR/Namespace/Command/Sys.pm lib/UR/Namespace/Command/Sys/ClassBrowser.pm lib/UR/Namespace/Command/Test.pm lib/UR/Namespace/Command/Test/Callcount.pm lib/UR/Namespace/Command/Test/Callcount/List.pm lib/UR/Namespace/Command/Test/Compile.pm lib/UR/Namespace/Command/Test/Eval.pm lib/UR/Namespace/Command/Test/Run.pm lib/UR/Namespace/Command/Test/TrackObjectRelease.pm lib/UR/Namespace/Command/Test/Use.pm lib/UR/Namespace/Command/Test/Window.pm lib/UR/Namespace/Command/Update.pm lib/UR/Namespace/Command/Update/ClassDiagram.pm lib/UR/Namespace/Command/Update/ClassesFromDb.pm lib/UR/Namespace/Command/Update/Doc.pm lib/UR/Namespace/Command/Update/Pod.pm lib/UR/Namespace/Command/Update/RenameClass.pm lib/UR/Namespace/Command/Update/RewriteClassHeader.pm lib/UR/Namespace/Command/Update/SchemaDiagram.pm lib/UR/Namespace/Command/Update/TabCompletionSpec.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp/Base.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp/Class.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp/File.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp/Index.pm lib/UR/Namespace/View/SchemaBrowser/CgiApp/Schema.pm lib/UR/Object.pm lib/UR/Object/Accessorized.pm lib/UR/Object/Command/FetchAndDo.pm lib/UR/Object/Command/List.pm lib/UR/Object/Command/List.pod lib/UR/Object/Command/List/Style.pm lib/UR/Object/Ghost.pm lib/UR/Object/Index.pm lib/UR/Object/Iterator.pm lib/UR/Object/Join.pm lib/UR/Object/Property.pm lib/UR/Object/Property/View/Default/Text.pm lib/UR/Object/Property/View/DescriptionLineItem/Text.pm lib/UR/Object/Property/View/ReferenceDescription/Text.pm lib/UR/Object/Set.pm lib/UR/Object/Set/View/Default/Html.pm lib/UR/Object/Set/View/Default/Json.pm lib/UR/Object/Set/View/Default/Text.pm lib/UR/Object/Set/View/Default/Xml.pm lib/UR/Object/Tag.pm lib/UR/Object/Type.pm lib/UR/Object/Type.pod lib/UR/Object/Type/AccessorWriter.pm lib/UR/Object/Type/AccessorWriter/Product.pm lib/UR/Object/Type/AccessorWriter/Sum.pm lib/UR/Object/Type/Initializer.pm lib/UR/Object/Type/Initializer.pod lib/UR/Object/Type/InternalAPI.pm lib/UR/Object/Type/ModuleWriter.pm lib/UR/Object/Type/View/AvailableViews/Json.pm lib/UR/Object/Type/View/AvailableViews/Xml.pm lib/UR/Object/Type/View/Default/Text.pm lib/UR/Object/Type/View/Default/Xml.pm lib/UR/Object/Value.pm lib/UR/Object/View.pm lib/UR/Object/View/Aspect.pm lib/UR/Object/View/Default/Gtk.pm lib/UR/Object/View/Default/Gtk2.pm lib/UR/Object/View/Default/Html.pm lib/UR/Object/View/Default/Json.pm lib/UR/Object/View/Default/Text.pm lib/UR/Object/View/Default/Xml.pm lib/UR/Object/View/Default/Xsl.pm lib/UR/Object/View/Lister/Text.pm lib/UR/Object/View/Static/Html.pm lib/UR/Object/View/Toolkit.pm lib/UR/Object/View/Toolkit/Text.pm lib/UR/ObjectDeprecated.pm lib/UR/ObjectV001removed.pm lib/UR/ObjectV04removed.pm lib/UR/Observer.pm lib/UR/Service/json.js lib/UR/Service/JsonRpcServer.pm lib/UR/Service/RPC/Executer.pm lib/UR/Service/RPC/Message.pm lib/UR/Service/RPC/Server.pm lib/UR/Service/RPC/TcpConnectionListener.pm lib/UR/Service/urinterface.js lib/UR/Singleton.pm lib/UR/Test.pm lib/UR/Util.pm lib/UR/Value.pm lib/UR/Value/ARRAY.pm lib/UR/Value/Blob.pm lib/UR/Value/Boolean.pm lib/UR/Value/Boolean/View/Default/Text.pm lib/UR/Value/CODE.pm lib/UR/Value/CSV.pm lib/UR/Value/DateTime.pm lib/UR/Value/Decimal.pm lib/UR/Value/DirectoryPath.pm lib/UR/Value/FilePath.pm lib/UR/Value/FilesystemPath.pm lib/UR/Value/Float.pm lib/UR/Value/FOF.pm lib/UR/Value/GLOB.pm lib/UR/Value/HASH.pm lib/UR/Value/Integer.pm lib/UR/Value/Iterator.pm lib/UR/Value/Number.pm lib/UR/Value/PerlReference.pm lib/UR/Value/REF.pm lib/UR/Value/SCALAR.pm lib/UR/Value/Set.pm lib/UR/Value/SloppyPrimitive.pm lib/UR/Value/String.pm lib/UR/Value/Text.pm lib/UR/Value/Timestamp.pm lib/UR/Value/URL.pm lib/UR/Value/View/Default/Html.pm lib/UR/Value/View/Default/Json.pm lib/UR/Value/View/Default/Text.pm lib/UR/Value/View/Default/Xml.pm lib/UR/Value/View/Default/Xml.t lib/UR/Vocabulary.pm LICENSE MANIFEST This list of files MANIFEST.SKIP META.yml pod/ur-define-class.pod pod/ur-define-datasource-file.pod pod/ur-define-datasource-mysql.pod pod/ur-define-datasource-oracle.pod pod/ur-define-datasource-pg.pod pod/ur-define-datasource-sqlite.pod pod/ur-define-datasource.pod pod/ur-define-db.pod pod/ur-define-namespace.pod pod/ur-define.pod pod/ur-describe.pod pod/ur-init.pod pod/ur-list-classes.pod pod/ur-list-modules.pod pod/ur-list-objects.pod pod/ur-list.pod pod/ur-old-diff-rewrite.pod pod/ur-old-diff-update.pod pod/ur-old-export-dbic-classes.pod pod/ur-old-info.pod pod/ur-old-redescribe.pod pod/ur-old.pod pod/ur-sys-class-browser.pod pod/ur-sys.pod pod/ur-test-callcount-list.pod pod/ur-test-callcount.pod pod/ur-test-compile.pod pod/ur-test-eval.pod pod/ur-test-run.pod pod/ur-test-track-object-release.pod pod/ur-test-use.pod pod/ur-test-window.pod pod/ur-test.pod pod/ur-update-class-diagram.pod pod/ur-update-classes-from-db.pod pod/ur-update-pod.pod pod/ur-update-rename-class.pod pod/ur-update-rewrite-class-header.pod pod/ur-update-schema-diagram.pod pod/ur-update-tab-completion-spec.pod pod/ur-update.pod pod/ur.pod README t/above.t t/alternate_namespace_layout/classes/URTAlternate/Person.pm t/alternate_namespace_layout/classes/URTAlternate/Vocabulary.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/Meta.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/Meta.sqlite3-dump t/alternate_namespace_layout/data_source/URTAlternate/DataSource/TheDB.pm t/alternate_namespace_layout/data_source/URTAlternate/DataSource/TheDB.sqlite3-dump t/alternate_namespace_layout/more_classes/URTAlternate/Car.pm t/alternate_namespace_layout/namespace/URTAlternate.pm t/alternate_namespace_layout/t/01_namespace.t t/alternate_namespace_layout/t/02_update_classes.t t/CdExample.pm t/CdExample/Artist.pm t/CdExample/Cd.pm t/CmdTest.pm t/CmdTest/C1.pm t/CmdTest/C2.pm t/CmdTest/C3.pm t/CmdTest/Stuff.pm t/CmdTest/t/01-mutual-resolution-via-to.t t/newnamespace/01_command_define_namespace.t t/Slimspace.pm t/ur-cachetest.pl t/urbenchmark.pl t/URT.pm t/URT/34Baseclass.pm t/URT/34Subclass.pm t/URT/38Primary.pm t/URT/38Related.pm t/URT/43Primary.pm t/URT/43Related.pm t/URT/Context/Testing.pm t/URT/DataSource/CircFk.pm t/URT/DataSource/Meta.pm t/URT/DataSource/Meta.sqlite3 t/URT/DataSource/Meta.sqlite3-dump t/URT/DataSource/Meta.sqlite3-schema t/URT/DataSource/SomeFile.pm t/URT/DataSource/SomeFileMux.pm t/URT/DataSource/SomeMySQL.pm t/URT/DataSource/SomeOracle.pm t/URT/DataSource/SomePostgreSQL.pm t/URT/DataSource/SomeSQLite.pm t/URT/ObjWithHash.pm t/URT/RAMThingy.pm t/URT/t/001_util_on_destroy.t t/URT/t/00_load.t t/URT/t/01_object.t t/URT/t/02_class_construction.t t/URT/t/03a_rules.t t/URT/t/03b_rule_constant_values.t t/URT/t/03b_rule_subsets.t t/URT/t/03c_rule_values.t t/URT/t/03d_rule_construction.t t/URT/t/03e_params_list.t t/URT/t/03f_rule_from_filter_string.t t/URT/t/03g_rule_constant_key_before.t t/URT/t/03h_rule_for_property_meta.t t/URT/t/03i_non_ur_types_as_values.t t/URT/t/03i_rule_hard_refs.t t/URT/t/03j_or_rules_with_meta.t t/URT/t/04a_sqlite.t t/URT/t/04a_sqlite_init_db_internal.t t/URT/t/04a_sqlite_sync_database.t t/URT/t/04b_mysql.t t/URT/t/04c_postresql.t t/URT/t/04d_oracle.t t/URT/t/04e_file.t t/URT/t/04e_file_sync_database.t t/URT/t/04e_file_track_open_close.t t/URT/t/04f_filemux.t t/URT/t/04f_filemux_sync_database.t t/URT/t/05_get_create_get.t t/URT/t/06_accessor_simple.t t/URT/t/07_create_get_simple.t t/URT/t/08_create_get_complex1.t t/URT/t/09_create_get_complex2.t t/URT/t/10_accessor_object.t t/URT/t/11_create_with_delegated_property.t t/URT/t/11b_via_to_without_type.t t/URT/t/11c_create_with_via_property.t t/URT/t/11d_create_with_single_delegated_property_via_is_many_property.t t/URT/t/12_properties_metadata_query.t t/URT/t/13a_messaging.t t/URT/t/13b_dump_message_inheritance.t t/URT/t/13c_message_observers.t t/URT/t/14_ghost_objects.t t/URT/t/15_singleton.t t/URT/t/16_viewer.t t/URT/t/17_accessor_object_basic.t t/URT/t/17b_mk_rw_accessor_signals_property_change.t t/URT/t/17c_rw_property_alias.t t/URT/t/18_indirect_accessor.t t/URT/t/19_calculated_accessor.t t/URT/t/20_has_many.t t/URT/t/20a_has_many_with_multiple_ids.t t/URT/t/21_observer.t t/URT/t/21b_load_observer_autosubclass.t t/URT/t/21c_load_observer_abstract_parent.t t/URT/t/21d_db_entity_observers.t t/URT/t/21e_old_subscription_api.t t/URT/t/21f_observer_priority.t t/URT/t/22_cached_get_with_subclasses.t t/URT/t/23_id_class_by_accessor.t t/URT/t/24_query_by_is_calculated.t t/URT/t/24_query_by_is_transient.t t/URT/t/24_query_via_method_call.t t/URT/t/25_recurse_get.t t/URT/t/26_indirect_mutator_with_where_via_is_many.t t/URT/t/27_get_with_limit_offset.t t/URT/t/28_dont_index_delegated_props.t t/URT/t/29_indirect_calculated_accessor.t t/URT/t/29b_join_calculated_accessor.t t/URT/t/29c_join_indirect_accessor.t t/URT/t/30_default_values.t t/URT/t/31_ref_as_value.t t/URT/t/32_ur_object_id.t t/URT/t/34_autouse_with_circular_ur_classdef.t t/URT/t/35_all_objects_are_loaded_subclass.t t/URT/t/36_superclass_already_loaded.t t/URT/t/37_caching_with_in_clause.t t/URT/t/37b_caching_with_in_clause.t t/URT/t/38_join_across_data_sources.t t/URT/t/39_has_many.t t/URT/t/40_has_many_direct.t t/URT/t/41_rpc_basic.t t/URT/t/42_rpc_between_processes.t t/URT/t/43_infer_values_from_rule.t t/URT/t/44_modulewriter.t t/URT/t/45_deleted_subclassed_objects_stay_deleted.t t/URT/t/45_rollback_deleted_object.t t/URT/t/46_meta_property_relationships.t t/URT/t/47_indirect_is_many_accessor.t t/URT/t/47b_indirect_is_many_accessor_mutable_with_id_class_by.t t/URT/t/47c_is_many_accessor_with_id_class_by.t t/URT/t/48_inline_datasources.t t/URT/t/49_complicated_get.t t/URT/t/49b_complicated_get_2.t t/URT/t/49c_complicated_get_3.t t/URT/t/49d_complicated_get_joining_through_view.t t/URT/t/49e_complicated_get_joining_through_view2.t t/URT/t/49f_complicated_get_indirect_id_by.t t/URT/t/49g_complicated_get_double_join.t t/URT/t/49h_complicated_get_double_join.t t/URT/t/49i_complicated_get_join_through_value_class.t t/URT/t/49j_complicated_get_join_ends_at_value_class.t t/URT/t/49k_complicated_get_joins_with_hangoff_filter.t t/URT/t/49l_complicated_get_id_by_attribute.t t/URT/t/49m_reverse_as_is_delegated.t t/URT/t/50_force_always_reload.t t/URT/t/50_get_and_reload.t t/URT/t/50_load_objects_that_stringify_false.t t/URT/t/50_unload_and_reload.t t/URT/t/50b_get_via_sql.t t/URT/t/51_get_with_hints.t t/URT/t/51b_unmatched_hints_query_cache.t t/URT/t/52_limit_cache_size.t t/URT/t/53_abandoned_iterator.t t/URT/t/54_valid_values.t t/URT/t/55_on_the_fly_metadb.t t/URT/t/55b_partial_metada_data.t t/URT/t/56_order_by_returns_items_in_order.t t/URT/t/56b_order_by_calculated_property.t t/URT/t/57_order_by_merge_new_objects.t t/URT/t/58_order_by_merge_changed_objects.t t/URT/t/59_get_merge_new_objs_with_db.t t/URT/t/60_get_merge_changed_objs_with_db.t t/URT/t/60_sql_query_hint.t t/URT/t/61_iterator.t t/URT/t/61_iterator_merge_changed_objs_with_db.t t/URT/t/61a_iterator_with_or_boolexpr.t t/URT/t/62_in_not_in_operator.t t/URT/t/62b_in_not_in_operator.t t/URT/t/63_view_text.t t/URT/t/63b_view_with_subviews.t t/URT/t/63c_view_with_subviews.t t/URT/t/63c_view_with_subviews.t.expected.cat_set.json t/URT/t/63c_view_with_subviews.t.expected.cat_set.text t/URT/t/63c_view_with_subviews.t.expected.cat_set.xml t/URT/t/63c_view_with_subviews.t.expected.person.json t/URT/t/63c_view_with_subviews.t.expected.person.text t/URT/t/63c_view_with_subviews.t.expected.person.xml t/URT/t/63d_delete_view.t t/URT/t/63e_enumerate_available_views.t t/URT/t/64_nullable_foreign_key_handling_on_insert_and_delete.t t/URT/t/65_reload_with_changing_db_data.t t/URT/t/66_nullable_hangoff_data.t t/URT/t/67_composite_id_with_id_class_by_rt55121.t t/URT/t/68_trapped_death_does_not_stack_trace.t t/URT/t/69_subclassify_by.t t/URT/t/69_subclassify_by_db.t t/URT/t/70_command_arg_processing.t t/URT/t/70_command_help_text.t t/URT/t/70c_command_tree_usage_text.t t/URT/t/71_ur_value.t t/URT/t/72_command_name_validation.t t/URT/t/73_opts_spec_creation_and_validation.t t/URT/t/74_xsl_view_url_convert.t t/URT/t/75_custom_loader.t t/URT/t/76_is_many_default_values.t t/URT/t/77_file_undef_value_handling.t t/URT/t/77_index_undef_value_handling.t t/URT/t/77_sql_undef_value_handling.t t/URT/t/78_get_by_subclass_params_load_properly.t t/URT/t/78b_get_by_subclass_property.t t/URT/t/79_like_operator.t t/URT/t/80_command_define_datasource.t t/URT/t/80b_namespace_command_base.t t/URT/t/80c_command_describe.t t/URT/t/80d_command_list.t t/URT/t/81_crud_custom_columnnames.t t/URT/t/82_boolexpr_op_underscore.t t/URT/t/82a_boolexpr_op_case_insensitive.t t/URT/t/83_commit_between_schemas.t t/URT/t/84_class_definition_errors.t t/URT/t/84b_implied_properties.t t/URT/t/85_avoid_loading_using_hints.t t/URT/t/85_method_meta.t t/URT/t/85b_avoid_loading_using_hints.t t/URT/t/86_custom_load.t t/URT/t/86b-custom-load-join.t t/URT/t/87_attributes_have.t t/URT/t/87_get_by_different_params_updates_query_cache.t t/URT/t/87_is_many_indirect_is_efficient.t t/URT/t/87a_many_to_many_query_is_efficient.t t/URT/t/87b_is_many_id_class_by_is_efficient.t t/URT/t/87c_query_by_is_many_indirect_is_efficient.t t/URT/t/87d_query_by_is_many_indirect_is_efficient.t t/URT/t/87e_missing_hangoff_data_is_efficient.t t/URT/t/87f_via_property_joins_to_itself.t t/URT/t/89_loading_with_boolexpr_evaluate.t t/URT/t/90_comparison_value_and_escape_character_to_regex.t t/URT/t/91_object_sets.t t/URT/t/91b_sets_count_with_changes.t t/URT/t/91c_set_relay.t t/URT/t/91d_basic_set.t t/URT/t/92_save_object_with_propertyless_column.t t/URT/t/93_namespace.t t/URT/t/93b_namespace_loaded_from_symlink.t t/URT/t/94_chain_join.t t/URT/t/94b_flatten_reframe.t t/URT/t/95_detect_db_deleted.t t/URT/t/95_normalize_property_description.t t/URT/t/95b_subclass_description_preprocessor_errors.t t/URT/t/95c_detect_changed_in_memory_filter.t t/URT/t/96_context_clear_cache.t t/URT/t/96b_ur_context_class_commit_triggers_observer.t t/URT/t/96c_ur_context_current_and_process.t t/URT/t/97_used_libs.t t/URT/t/98_ur_update.t t/URT/t/99_transaction-failed_commit_rollback.t t/URT/t/99_transaction-observers.t t/URT/t/99_transaction.t t/URT/t/file_datasource/path_spec_expansion.t t/URT/t/file_datasource/read.t t/URT/t/file_datasource/read_columns_from_header.t t/URT/t/file_datasource/read_efficiency.t t/URT/t/file_datasource/read_files_as_tables.t t/URT/t/file_datasource/read_linenum_as_column.t t/URT/t/file_datasource/read_multichar_record_sep.t t/URT/t/file_datasource/read_order_by.t t/URT/t/file_datasource/write.t t/URT/t/mro.t t/URT/Thingy.pm t/URT/Vocabulary.pm t/Vending.pm t/Vending/Coin.pm t/Vending/CoinType.pm t/Vending/Command.pm t/Vending/Command/Buy.pm t/Vending/Command/CoinReturn.pm t/Vending/Command/Dime.pm t/Vending/Command/Dollar.pm t/Vending/Command/InsertMoney.pm t/Vending/Command/Menu.pm t/Vending/Command/Nickel.pm t/Vending/Command/Outputter.pm t/Vending/Command/Quarter.pm t/Vending/Command/Service.pm t/Vending/Command/Service/Add.pm t/Vending/Command/Service/Add/Change.pm t/Vending/Command/Service/Add/Inventory.pm t/Vending/Command/Service/Add/Slot.pm t/Vending/Command/Service/ConfigureSlot.pm t/Vending/Command/Service/EmptyBank.pm t/Vending/Command/Service/RemoveSlot.pm t/Vending/Command/Service/Show.pm t/Vending/Command/Service/Show/Bank.pm t/Vending/Command/Service/Show/Change.pm t/Vending/Command/Service/Show/Inventory.pm t/Vending/Command/Service/Show/Money.pm t/Vending/Command/Service/Show/Slots.pm t/Vending/Content.pm t/Vending/ContentType.pm t/Vending/DataSource/coin_types.tsv t/Vending/DataSource/CoinType.pm t/Vending/DataSource/Machine.pm t/Vending/DataSource/Machine.sqlite3-dump t/Vending/DataSource/Meta.pm t/Vending/DataSource/Meta.sqlite3-dump t/Vending/get_coin_by_value.pl t/Vending/Machine.pm t/Vending/machine_classes_1.uxf t/Vending/MachineLocation.pm t/Vending/Merchandise.pm t/Vending/notes.txt t/Vending/Product.pm t/Vending/ReturnedItem.pm t/Vending/t/buy_a_different_change.t t/Vending/t/buy_a_get_change_back.t t/Vending/t/buy_a_not_enough_change.t t/Vending/t/buy_b_not_enough_money.t t/Vending/t/buy_b_with_exact_change.t t/Vending/t/coin_return.t t/Vending/vend t/Vending/vend_interactive.pl t/Vending/Vocabulary.pm META.json META.json000444023532023421 11322712121654174 13310 0ustar00abrummetgsc000000000000UR-0.41{ "abstract" : "rich declarative transactional objects", "author" : [ "Anthony Brummett brummett@cpan.org", "Scott Smith sakoht@cpan.org" ], "dynamic_config" : 1, "generated_by" : "Module::Build version 0.3901, CPAN::Meta::Converter version 2.120630", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "UR", "prereqs" : { "configure" : { "requires" : { "Module::Build" : "0.39" } }, "runtime" : { "requires" : { "Carp" : "0", "Class::AutoloadCAN" : "0.03", "Class::Autouse" : "2.0", "Clone::PP" : "1.02", "DBD::SQLite" : "1.14", "DBI" : "1.601", "Data::Compare" : "0.13", "Data::UUID" : "0.148", "Date::Format" : "0", "Devel::GlobalDestruction" : "0", "File::Basename" : "2.73", "File::Path" : "0", "File::Temp" : "0", "FreezeThaw" : "0.43", "Getopt::Complete" : "0.26", "JSON" : "0", "Lingua::EN::Inflect" : "1.88", "List::MoreUtils" : "0", "MRO::Compat" : "0", "Path::Class" : "0", "Pod::Simple::HTML" : "3.03", "Pod::Simple::Text" : "2.02", "Sub::Install" : "0.924", "Sub::Name" : "0.04", "Sys::Hostname" : "1.11", "Test::Fork" : "0", "Text::Diff" : "0.35", "Text::Glob" : "0", "YAML" : "0", "perl" : "v5.8.7", "version" : "0" } } }, "provides" : { "Command" : { "file" : "lib/Command.pm", "version" : "0.41" }, "Command::DynamicSubCommands" : { "file" : "lib/Command/DynamicSubCommands.pm", "version" : 0 }, "Command::Shell" : { "file" : "lib/Command/Shell.pm", "version" : 0 }, "Command::SubCommandFactory" : { "file" : "lib/Command/SubCommandFactory.pm", "version" : 0 }, "Command::Test" : { "file" : "lib/Command/Test.pm", "version" : 0 }, "Command::Test::Echo" : { "file" : "lib/Command/Test/Echo.pm", "version" : 0 }, "Command::Test::Tree1" : { "file" : "lib/Command/Test/Tree1.pm", "version" : 0 }, "Command::Test::Tree1::Echo1" : { "file" : "lib/Command/Test/Tree1/Echo1.pm", "version" : 0 }, "Command::Test::Tree1::Echo2" : { "file" : "lib/Command/Test/Tree1/Echo2.pm", "version" : 0 }, "Command::Tree" : { "file" : "lib/Command/Tree.pm", "version" : "0.41" }, "Command::V1" : { "file" : "lib/Command/V1.pm", "version" : "0.41" }, "Command::V2" : { "file" : "lib/Command/V2.pm", "version" : "0.41" }, "Devel::callsfrom" : { "file" : "lib/Devel/callcount.pm", "version" : 0 }, "My::TAP::Parser::Iterator::Process::LSF" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : 0 }, "My::TAP::Parser::IteratorFactory::LSF" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : 0 }, "My::TAP::Parser::Multiplexer" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : 0 }, "My::TAP::Parser::Scheduler" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : 0 }, "My::TAP::Parser::Timer" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : 0 }, "UR" : { "file" : "lib/UR.pm", "version" : "0.41" }, "UR::All" : { "file" : "lib/UR/All.pm", "version" : "0.41" }, "UR::BoolExpr" : { "file" : "lib/UR/BoolExpr.pm", "version" : "0.41" }, "UR::BoolExpr::BxParser" : { "file" : "lib/UR/BoolExpr/BxParser.pm", "version" : 0 }, "UR::BoolExpr::BxParser::Yapp::Driver" : { "file" : "lib/UR/BoolExpr/BxParser.pm", "version" : "1.05" }, "UR::BoolExpr::Template" : { "file" : "lib/UR/BoolExpr/Template.pm", "version" : "0.41" }, "UR::BoolExpr::Template::And" : { "file" : "lib/UR/BoolExpr/Template/And.pm", "version" : "0.41" }, "UR::BoolExpr::Template::Composite" : { "file" : "lib/UR/BoolExpr/Template/Composite.pm", "version" : "0.41" }, "UR::BoolExpr::Template::Or" : { "file" : "lib/UR/BoolExpr/Template/Or.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::Between" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Between.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::Equals" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::False" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/False.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::GreaterThan" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::In" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/In.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::LessOrEqual" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::LessThan" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::Like" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Like.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::Matches" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::NotEqual" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotEqual.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::NotIn" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::NotLike" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm", "version" : "0.41" }, "UR::BoolExpr::Template::PropertyComparison::True" : { "file" : "lib/UR/BoolExpr/Template/PropertyComparison/True.pm", "version" : "0.41" }, "UR::BoolExpr::Util" : { "file" : "lib/UR/BoolExpr/Util.pm", "version" : "0.41" }, "UR::Change" : { "file" : "lib/UR/Change.pm", "version" : "0.41" }, "UR::Context" : { "file" : "lib/UR/Context.pm", "version" : "0.41" }, "UR::Context::DefaultRoot" : { "file" : "lib/UR/Context/DefaultRoot.pm", "version" : "0.41" }, "UR::Context::LoadingIterator" : { "file" : "lib/UR/Context/LoadingIterator.pm", "version" : "0.41" }, "UR::Context::ObjectFabricator" : { "file" : "lib/UR/Context/ObjectFabricator.pm", "version" : "0.41" }, "UR::Context::Process" : { "file" : "lib/UR/Context/Process.pm", "version" : "0.41" }, "UR::Context::Root" : { "file" : "lib/UR/Context/Root.pm", "version" : "0.41" }, "UR::Context::Transaction" : { "file" : "lib/UR/Context/Transaction.pm", "version" : "0.41" }, "UR::DBI" : { "file" : "lib/UR/DBI.pm", "version" : "0.41" }, "UR::DBI::Report" : { "file" : "lib/UR/DBI/Report.pm", "version" : "0.41" }, "UR::DBI::db" : { "file" : "lib/UR/DBI.pm", "version" : 0 }, "UR::DBI::st" : { "file" : "lib/UR/DBI.pm", "version" : 0 }, "UR::DataSource" : { "file" : "lib/UR/DataSource.pm", "version" : "0.41" }, "UR::DataSource::CSV" : { "file" : "lib/UR/DataSource/CSV.pm", "version" : "0.41" }, "UR::DataSource::Code" : { "file" : "lib/UR/DataSource/Code.pm", "version" : "0.41" }, "UR::DataSource::Default" : { "file" : "lib/UR/DataSource/Default.pm", "version" : "0.41" }, "UR::DataSource::File" : { "file" : "lib/UR/DataSource/File.pm", "version" : "0.41" }, "UR::DataSource::FileMux" : { "file" : "lib/UR/DataSource/FileMux.pm", "version" : "0.41" }, "UR::DataSource::Filesystem" : { "file" : "lib/UR/DataSource/Filesystem.pm", "version" : "0.41" }, "UR::DataSource::Meta" : { "file" : "lib/UR/DataSource/Meta.pm", "version" : "0.41" }, "UR::DataSource::MySQL" : { "file" : "lib/UR/DataSource/MySQL.pm", "version" : "0.41" }, "UR::DataSource::Oracle" : { "file" : "lib/UR/DataSource/Oracle.pm", "version" : "0.41" }, "UR::DataSource::Pg" : { "file" : "lib/UR/DataSource/Pg.pm", "version" : "0.41" }, "UR::DataSource::QueryPlan" : { "file" : "lib/UR/DataSource/QueryPlan.pm", "version" : "0.41" }, "UR::DataSource::RDBMS" : { "file" : "lib/UR/DataSource/RDBMS.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::BitmapIndex" : { "file" : "lib/UR/DataSource/RDBMS/BitmapIndex.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::Entity" : { "file" : "lib/UR/DataSource/RDBMS/Entity.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::FkConstraint" : { "file" : "lib/UR/DataSource/RDBMS/FkConstraint.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::FkConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/FkConstraintColumn.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::PkConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/PkConstraintColumn.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::Table" : { "file" : "lib/UR/DataSource/RDBMS/Table.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::Table::View::Default::Text" : { "file" : "lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::TableColumn" : { "file" : "lib/UR/DataSource/RDBMS/TableColumn.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::TableColumn::View::Default::Text" : { "file" : "lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm", "version" : "0.41" }, "UR::DataSource::RDBMS::UniqueConstraintColumn" : { "file" : "lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm", "version" : "0.41" }, "UR::DataSource::SQLite" : { "file" : "lib/UR/DataSource/SQLite.pm", "version" : "0.41" }, "UR::DataSource::ValueDomain" : { "file" : "lib/UR/DataSource/ValueDomain.pm", "version" : "0.41" }, "UR::Debug" : { "file" : "lib/UR/Debug.pm", "version" : "0.41" }, "UR::DeletedRef" : { "file" : "lib/UR/DeletedRef.pm", "version" : "0.41" }, "UR::Doc::Pod2Html" : { "file" : "lib/UR/Doc/Pod2Html.pm", "version" : "0.41" }, "UR::Doc::Section" : { "file" : "lib/UR/Doc/Section.pm", "version" : "0.41" }, "UR::Doc::Writer" : { "file" : "lib/UR/Doc/Writer.pm", "version" : "0.41" }, "UR::Doc::Writer::Html" : { "file" : "lib/UR/Doc/Writer/Html.pm", "version" : "0.41" }, "UR::Doc::Writer::Pod" : { "file" : "lib/UR/Doc/Writer/Pod.pm", "version" : "0.41" }, "UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES" : { "file" : "lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_BASE" : { "file" : "lib/UR/Env/UR_CONTEXT_BASE.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER" : { "file" : "lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER" : { "file" : "lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_LIBS" : { "file" : "lib/UR/Env/UR_USED_LIBS.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_MONITOR_QUERY" : { "file" : "lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm", "version" : "0.41" }, "UR::Env::UR_CONTEXT_ROOT" : { "file" : "lib/UR/Env/UR_CONTEXT_ROOT.pm", "version" : "0.41" }, "UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT" : { "file" : "lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm", "version" : "0.41" }, "UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm", "version" : "0.41" }, "UR::Env::UR_DBI_EXPLAIN_SQL_IF" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm", "version" : "0.41" }, "UR::Env::UR_DBI_EXPLAIN_SQL_MATCH" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm", "version" : "0.41" }, "UR::Env::UR_DBI_EXPLAIN_SQL_SLOW" : { "file" : "lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm", "version" : "0.41" }, "UR::Env::UR_DBI_MONITOR_DML" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_DML.pm", "version" : "0.41" }, "UR::Env::UR_DBI_MONITOR_EVERY_FETCH" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm", "version" : "0.41" }, "UR::Env::UR_DBI_MONITOR_SQL" : { "file" : "lib/UR/Env/UR_DBI_MONITOR_SQL.pm", "version" : "0.41" }, "UR::Env::UR_DBI_NO_COMMIT" : { "file" : "lib/UR/Env/UR_DBI_NO_COMMIT.pm", "version" : "0.41" }, "UR::Env::UR_DBI_SUMMARIZE_SQL" : { "file" : "lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm", "version" : "0.41" }, "UR::Env::UR_DEBUG_OBJECT_PRUNING" : { "file" : "lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm", "version" : "0.41" }, "UR::Env::UR_DEBUG_OBJECT_RELEASE" : { "file" : "lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm", "version" : "0.41" }, "UR::Env::UR_IGNORE" : { "file" : "lib/UR/Env/UR_IGNORE.pm", "version" : "0.41" }, "UR::Env::UR_NO_REQUIRE_USER_VERIFY" : { "file" : "lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm", "version" : "0.41" }, "UR::Env::UR_NR_CPU" : { "file" : "lib/UR/Env/UR_NR_CPU.pm", "version" : "0.41" }, "UR::Env::UR_RUN_LONG_TESTS" : { "file" : "lib/UR/Env/UR_RUN_LONG_TESTS.pm", "version" : "0.41" }, "UR::Env::UR_STACK_DUMP_ON_DIE" : { "file" : "lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm", "version" : "0.41" }, "UR::Env::UR_STACK_DUMP_ON_WARN" : { "file" : "lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm", "version" : "0.41" }, "UR::Env::UR_TEST_FILLDB" : { "file" : "lib/UR/Env/UR_TEST_FILLDB.pm", "version" : "0.41" }, "UR::Env::UR_TEST_QUIET" : { "file" : "lib/UR/Env/UR_TEST_QUIET.pm", "version" : "0.41" }, "UR::Env::UR_USED_MODS" : { "file" : "lib/UR/Env/UR_USED_MODS.pm", "version" : "0.41" }, "UR::Env::UR_USE_ANY" : { "file" : "lib/UR/Env/UR_USE_ANY.pm", "version" : "0.41" }, "UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS" : { "file" : "lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm", "version" : "0.41" }, "UR::Exit" : { "file" : "lib/UR/Exit.pm", "version" : "0.41" }, "UR::ModuleBase" : { "file" : "lib/UR/ModuleBase.pm", "version" : "0.41" }, "UR::ModuleBase::Message" : { "file" : "lib/UR/ObjectDeprecated.pm", "version" : 0 }, "UR::ModuleBuild" : { "file" : "lib/UR/ModuleBuild.pm", "version" : 0 }, "UR::ModuleConfig" : { "file" : "lib/UR/ModuleConfig.pm", "version" : "0.41" }, "UR::ModuleLoader" : { "file" : "lib/UR/ModuleLoader.pm", "version" : "0.41" }, "UR::Namespace" : { "file" : "lib/UR/Namespace.pm", "version" : "0.41" }, "UR::Namespace::Command" : { "file" : "lib/UR/Namespace/Command.pm", "version" : "0.41" }, "UR::Namespace::Command::Base" : { "file" : "lib/UR/Namespace/Command/Base.pm", "version" : "0.41" }, "UR::Namespace::Command::Define" : { "file" : "lib/UR/Namespace/Command/Define.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Class" : { "file" : "lib/UR/Namespace/Command/Define/Class.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource" : { "file" : "lib/UR/Namespace/Command/Define/Datasource.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::File" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/File.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::Mysql" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Mysql.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::Oracle" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Oracle.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::Pg" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Pg.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::Rdbms" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Datasource::Sqlite" : { "file" : "lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Db" : { "file" : "lib/UR/Namespace/Command/Define/Db.pm", "version" : "0.41" }, "UR::Namespace::Command::Define::Namespace" : { "file" : "lib/UR/Namespace/Command/Define/Namespace.pm", "version" : "0.41" }, "UR::Namespace::Command::Init" : { "file" : "lib/UR/Namespace/Command/Init.pm", "version" : "0.41" }, "UR::Namespace::Command::List" : { "file" : "lib/UR/Namespace/Command/List.pm", "version" : "0.41" }, "UR::Namespace::Command::List::Classes" : { "file" : "lib/UR/Namespace/Command/List/Classes.pm", "version" : "0.41" }, "UR::Namespace::Command::List::Modules" : { "file" : "lib/UR/Namespace/Command/List/Modules.pm", "version" : "0.41" }, "UR::Namespace::Command::List::Objects" : { "file" : "lib/UR/Namespace/Command/List/Objects.pm", "version" : "0.41" }, "UR::Namespace::Command::Old" : { "file" : "lib/UR/Namespace/Command/Old.pm", "version" : "0.41" }, "UR::Namespace::Command::Old::DiffRewrite" : { "file" : "lib/UR/Namespace/Command/Old/DiffRewrite.pm", "version" : "0.41" }, "UR::Namespace::Command::Old::DiffUpdate" : { "file" : "lib/UR/Namespace/Command/Old/DiffUpdate.pm", "version" : "0.41" }, "UR::Namespace::Command::Old::ExportDbicClasses" : { "file" : "lib/UR/Namespace/Command/Old/ExportDbicClasses.pm", "version" : "0.41" }, "UR::Namespace::Command::Old::Info" : { "file" : "lib/UR/Namespace/Command/Old/Info.pm", "version" : "0.41" }, "UR::Namespace::Command::Old::Redescribe" : { "file" : "lib/UR/Namespace/Command/Old/Redescribe.pm", "version" : "0.41" }, "UR::Namespace::Command::RunsOnModulesInTree" : { "file" : "lib/UR/Namespace/Command/RunsOnModulesInTree.pm", "version" : "0.41" }, "UR::Namespace::Command::Show" : { "file" : "lib/UR/Namespace/Command/Show.pm", "version" : 0 }, "UR::Namespace::Command::Show::Properties" : { "file" : "lib/UR/Namespace/Command/Show/Properties.pm", "version" : "0.41" }, "UR::Namespace::Command::Show::Schema" : { "file" : "lib/UR/Namespace/Command/Show/Schema.pm", "version" : 0 }, "UR::Namespace::Command::Show::Subclasses" : { "file" : "lib/UR/Namespace/Command/Show/Subclasses.pm", "version" : 0 }, "UR::Namespace::Command::Sys" : { "file" : "lib/UR/Namespace/Command/Sys.pm", "version" : "0.41" }, "UR::Namespace::Command::Sys::ClassBrowser" : { "file" : "lib/UR/Namespace/Command/Sys/ClassBrowser.pm", "version" : "0.41" }, "UR::Namespace::Command::Test" : { "file" : "lib/UR/Namespace/Command/Test.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Callcount" : { "file" : "lib/UR/Namespace/Command/Test/Callcount.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Callcount::List" : { "file" : "lib/UR/Namespace/Command/Test/Callcount/List.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Compile" : { "file" : "lib/UR/Namespace/Command/Test/Compile.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Eval" : { "file" : "lib/UR/Namespace/Command/Test/Eval.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Run" : { "file" : "lib/UR/Namespace/Command/Test/Run.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::TrackObjectRelease" : { "file" : "lib/UR/Namespace/Command/Test/TrackObjectRelease.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Use" : { "file" : "lib/UR/Namespace/Command/Test/Use.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Window" : { "file" : "lib/UR/Namespace/Command/Test/Window.pm", "version" : "0.41" }, "UR::Namespace::Command::Test::Window::Tk" : { "file" : "lib/UR/Namespace/Command/Test/Window.pm", "version" : 0 }, "UR::Namespace::Command::Update" : { "file" : "lib/UR/Namespace/Command/Update.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::ClassDiagram" : { "file" : "lib/UR/Namespace/Command/Update/ClassDiagram.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::ClassesFromDb" : { "file" : "lib/UR/Namespace/Command/Update/ClassesFromDb.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::Doc" : { "file" : "lib/UR/Namespace/Command/Update/Doc.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::Pod" : { "file" : "lib/UR/Namespace/Command/Update/Pod.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::RenameClass" : { "file" : "lib/UR/Namespace/Command/Update/RenameClass.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::RewriteClassHeader" : { "file" : "lib/UR/Namespace/Command/Update/RewriteClassHeader.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::SchemaDiagram" : { "file" : "lib/UR/Namespace/Command/Update/SchemaDiagram.pm", "version" : "0.41" }, "UR::Namespace::Command::Update::TabCompletionSpec" : { "file" : "lib/UR/Namespace/Command/Update/TabCompletionSpec.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp::Base" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp/Base.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp::Class" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp/Class.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp::File" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp/File.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp::Index" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp/Index.pm", "version" : "0.41" }, "UR::Namespace::View::SchemaBrowser::CgiApp::Schema" : { "file" : "lib/UR/Namespace/View/SchemaBrowser/CgiApp/Schema.pm", "version" : "0.41" }, "UR::Object" : { "file" : "lib/UR/Object.pm", "version" : "0.41" }, "UR::Object::Accessorized" : { "file" : "lib/UR/Object/Accessorized.pm", "version" : "0.41" }, "UR::Object::Command::FetchAndDo" : { "file" : "lib/UR/Object/Command/FetchAndDo.pm", "version" : "0.41" }, "UR::Object::Command::List" : { "file" : "lib/UR/Object/Command/List.pm", "version" : "0.41" }, "UR::Object::Command::List::Csv" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Html" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Newtext" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Pretty" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Style" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : "0.41" }, "UR::Object::Command::List::Text" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Tsv" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Command::List::Xml" : { "file" : "lib/UR/Object/Command/List/Style.pm", "version" : 0 }, "UR::Object::Ghost" : { "file" : "lib/UR/Object/Ghost.pm", "version" : "0.41" }, "UR::Object::Index" : { "file" : "lib/UR/Object/Index.pm", "version" : "0.41" }, "UR::Object::Iterator" : { "file" : "lib/UR/Object/Iterator.pm", "version" : "0.41" }, "UR::Object::Join" : { "file" : "lib/UR/Object/Join.pm", "version" : "0.41" }, "UR::Object::Property" : { "file" : "lib/UR/Object/Property.pm", "version" : "0.41" }, "UR::Object::Property::View::Default::Text" : { "file" : "lib/UR/Object/Property/View/Default/Text.pm", "version" : "0.41" }, "UR::Object::Property::View::DescriptionLineItem::Text" : { "file" : "lib/UR/Object/Property/View/DescriptionLineItem/Text.pm", "version" : "0.41" }, "UR::Object::Property::View::ReferenceDescription::Text" : { "file" : "lib/UR/Object/Property/View/ReferenceDescription/Text.pm", "version" : "0.41" }, "UR::Object::Set" : { "file" : "lib/UR/Object/Set.pm", "version" : "0.41" }, "UR::Object::Set::View::Default::Html" : { "file" : "lib/UR/Object/Set/View/Default/Html.pm", "version" : "0.41" }, "UR::Object::Set::View::Default::Json" : { "file" : "lib/UR/Object/Set/View/Default/Json.pm", "version" : "0.41" }, "UR::Object::Set::View::Default::Text" : { "file" : "lib/UR/Object/Set/View/Default/Text.pm", "version" : "0.41" }, "UR::Object::Set::View::Default::Xml" : { "file" : "lib/UR/Object/Set/View/Default/Xml.pm", "version" : "0.41" }, "UR::Object::Tag" : { "file" : "lib/UR/Object/Tag.pm", "version" : "0.41" }, "UR::Object::Type" : { "file" : "lib/UR/Object/Type.pm", "version" : "0.41" }, "UR::Object::Type::AccessorWriter" : { "file" : "lib/UR/Object/Type/AccessorWriter.pm", "version" : 0 }, "UR::Object::Type::AccessorWriter::Product" : { "file" : "lib/UR/Object/Type/AccessorWriter/Product.pm", "version" : "0.41" }, "UR::Object::Type::AccessorWriter::Sum" : { "file" : "lib/UR/Object/Type/AccessorWriter/Sum.pm", "version" : "0.41" }, "UR::Object::Type::Initializer" : { "file" : "lib/UR/Object/Type/Initializer.pm", "version" : 0 }, "UR::Object::Type::ModuleWriter" : { "file" : "lib/UR/Object/Type/ModuleWriter.pm", "version" : 0 }, "UR::Object::Type::View::AvailableViews::Json" : { "file" : "lib/UR/Object/Type/View/AvailableViews/Json.pm", "version" : "0.41" }, "UR::Object::Type::View::AvailableViews::Xml" : { "file" : "lib/UR/Object/Type/View/AvailableViews/Xml.pm", "version" : "0.41" }, "UR::Object::Type::View::Default::Text" : { "file" : "lib/UR/Object/Type/View/Default/Text.pm", "version" : "0.41" }, "UR::Object::Type::View::Default::Xml" : { "file" : "lib/UR/Object/Type/View/Default/Xml.pm", "version" : "0.41" }, "UR::Object::Value" : { "file" : "lib/UR/Object/Value.pm", "version" : "0.41" }, "UR::Object::View" : { "file" : "lib/UR/Object/View.pm", "version" : "0.41" }, "UR::Object::View::Aspect" : { "file" : "lib/UR/Object/View/Aspect.pm", "version" : "0.41" }, "UR::Object::View::Default::Gtk" : { "file" : "lib/UR/Object/View/Default/Gtk.pm", "version" : "0.41" }, "UR::Object::View::Default::Gtk2" : { "file" : "lib/UR/Object/View/Default/Gtk2.pm", "version" : "0.41" }, "UR::Object::View::Default::Html" : { "file" : "lib/UR/Object/View/Default/Html.pm", "version" : "0.41" }, "UR::Object::View::Default::Json" : { "file" : "lib/UR/Object/View/Default/Json.pm", "version" : "0.41" }, "UR::Object::View::Default::Text" : { "file" : "lib/UR/Object/View/Default/Text.pm", "version" : "0.41" }, "UR::Object::View::Default::Xml" : { "file" : "lib/UR/Object/View/Default/Xml.pm", "version" : "0.41" }, "UR::Object::View::Default::Xsl" : { "file" : "lib/UR/Object/View/Default/Xsl.pm", "version" : "0.41" }, "UR::Object::View::Lister::Text" : { "file" : "lib/UR/Object/View/Lister/Text.pm", "version" : "0.41" }, "UR::Object::View::Static::Html" : { "file" : "lib/UR/Object/View/Static/Html.pm", "version" : "0.41" }, "UR::Object::View::Toolkit" : { "file" : "lib/UR/Object/View/Toolkit.pm", "version" : "0.41" }, "UR::Object::View::Toolkit::Text" : { "file" : "lib/UR/Object/View/Toolkit/Text.pm", "version" : "0.41" }, "UR::Observer" : { "file" : "lib/UR/Observer.pm", "version" : "0.41" }, "UR::Service::JsonRpcServer" : { "file" : "lib/UR/Service/JsonRpcServer.pm", "version" : "0.41" }, "UR::Service::RPC::Executer" : { "file" : "lib/UR/Service/RPC/Executer.pm", "version" : "0.41" }, "UR::Service::RPC::Message" : { "file" : "lib/UR/Service/RPC/Message.pm", "version" : "0.41" }, "UR::Service::RPC::Server" : { "file" : "lib/UR/Service/RPC/Server.pm", "version" : "0.41" }, "UR::Service::RPC::TcpConnectionListener" : { "file" : "lib/UR/Service/RPC/TcpConnectionListener.pm", "version" : "0.41" }, "UR::Singleton" : { "file" : "lib/UR/Singleton.pm", "version" : "0.41" }, "UR::Test" : { "file" : "lib/UR/Test.pm", "version" : "0.41" }, "UR::Util" : { "file" : "lib/UR/Util.pm", "version" : "0.41" }, "UR::Value" : { "file" : "lib/UR/Value.pm", "version" : "0.41" }, "UR::Value::ARRAY" : { "file" : "lib/UR/Value/ARRAY.pm", "version" : "0.41" }, "UR::Value::Blob" : { "file" : "lib/UR/Value/Blob.pm", "version" : "0.41" }, "UR::Value::Boolean" : { "file" : "lib/UR/Value/Boolean.pm", "version" : "0.41" }, "UR::Value::Boolean::View::Default::Text" : { "file" : "lib/UR/Value/Boolean/View/Default/Text.pm", "version" : "0.41" }, "UR::Value::CODE" : { "file" : "lib/UR/Value/CODE.pm", "version" : "0.41" }, "UR::Value::CSV" : { "file" : "lib/UR/Value/CSV.pm", "version" : "0.41" }, "UR::Value::DateTime" : { "file" : "lib/UR/Value/DateTime.pm", "version" : "0.41" }, "UR::Value::Decimal" : { "file" : "lib/UR/Value/Decimal.pm", "version" : "0.41" }, "UR::Value::DirectoryPath" : { "file" : "lib/UR/Value/DirectoryPath.pm", "version" : "0.41" }, "UR::Value::FOF" : { "file" : "lib/UR/Value/FOF.pm", "version" : "0.41" }, "UR::Value::FilePath" : { "file" : "lib/UR/Value/FilePath.pm", "version" : "0.41" }, "UR::Value::FilesystemPath" : { "file" : "lib/UR/Value/FilesystemPath.pm", "version" : "0.41" }, "UR::Value::Float" : { "file" : "lib/UR/Value/Float.pm", "version" : "0.41" }, "UR::Value::GLOB" : { "file" : "lib/UR/Value/GLOB.pm", "version" : "0.41" }, "UR::Value::HASH" : { "file" : "lib/UR/Value/HASH.pm", "version" : "0.41" }, "UR::Value::Integer" : { "file" : "lib/UR/Value/Integer.pm", "version" : "0.41" }, "UR::Value::Iterator" : { "file" : "lib/UR/Value/Iterator.pm", "version" : "0.41" }, "UR::Value::Number" : { "file" : "lib/UR/Value/Number.pm", "version" : "0.41" }, "UR::Value::PerlReference" : { "file" : "lib/UR/Value/PerlReference.pm", "version" : "0.41" }, "UR::Value::REF" : { "file" : "lib/UR/Value/REF.pm", "version" : "0.41" }, "UR::Value::SCALAR" : { "file" : "lib/UR/Value/SCALAR.pm", "version" : "0.41" }, "UR::Value::Set" : { "file" : "lib/UR/Value/Set.pm", "version" : "0.41" }, "UR::Value::SloppyPrimitive" : { "file" : "lib/UR/Value/SloppyPrimitive.pm", "version" : "0.41" }, "UR::Value::String" : { "file" : "lib/UR/Value/String.pm", "version" : "0.41" }, "UR::Value::Text" : { "file" : "lib/UR/Value/Text.pm", "version" : "0.41" }, "UR::Value::Timestamp" : { "file" : "lib/UR/Value/Timestamp.pm", "version" : "0.41" }, "UR::Value::URL" : { "file" : "lib/UR/Value/URL.pm", "version" : "0.41" }, "UR::Value::View::Default::Html" : { "file" : "lib/UR/Value/View/Default/Html.pm", "version" : 0 }, "UR::Value::View::Default::Json" : { "file" : "lib/UR/Value/View/Default/Json.pm", "version" : 0 }, "UR::Value::View::Default::Text" : { "file" : "lib/UR/Value/View/Default/Text.pm", "version" : 0 }, "UR::Value::View::Default::Xml" : { "file" : "lib/UR/Value/View/Default/Xml.pm", "version" : 0 }, "UR::Vocabulary" : { "file" : "lib/UR/Vocabulary.pm", "version" : "0.41" }, "above" : { "file" : "lib/above.pm", "version" : "0.02" }, "class_name" : { "file" : "lib/UR/Object/Type/Initializer.pm", "version" : "2" } }, "release_status" : "testing", "resources" : { "license" : [ "http://dev.perl.org/licenses/" ] }, "version" : "0.41" } Changes000444023532023421 3753212121654175 13147 0ustar00abrummetgsc000000000000UR-0.41Revision history for UR 0.41 2013-03-18 above.pm now imports symbols into the caller's package Fix for database connections after fork() in the child process Fixes for command-line parsing, implied property metadata and database joins Many test updates to work on more architectures 0.40 2013-02-25 RDBMS data sources now have infrastructure for comparing text and non-text columns during a join. When a number or date column is joined with a text column, the non-text column is converted with the to_char() function in the Oracle data source. An object-type property's default_value can now be specified using a hashref of keys/values. Property definitions can now include example_values - a listref of values shown to the user in the autogenerated documentation. Documentation for the Object Lister base command is expanded. 0.392 2013-01-31 Changed the name for the Yapp driver package to avoid a CPAN warning about unauthorized use of their namespace 0.39 2013-01-30 Better support for PostgreSQL. It is now on par with Oracle. New datasource UR::DataSource::Filesystem. It obsoletes UR::DataSource::File and UR::DataSource::FileMux, and is more flexible. Classes can specify a query hint when they are used as the primary class of a get() or when they are involved in a join. BoolExprs with an or-clause now support hints and order-by correctly. Messaging methods (error_message(), status_message(), etc) now trigger observers of the same name. This means any number of message observers can be attached at any point in the class hierarchy. Using chained delegated properties with the dot-syntax (object.delegate.prop) is accepted in more places. Better support for queries using direct SQL. Many fixes for the Boolean Expression syntax parser. Besides fixing bugs, it now supports more operators and understands 'offset' and 'limit'. Support for defining a property that is an alias for another. Fixes for remaining connected to databases after fork(). Optimization for the case where a delegation goes through an abstract class with no data source and back to the original data source. It now does one query instead of many. Improvements to the Command API documentation. Removed some deps on XML-related modules. 0.38 2012-03-28 Bug fixes to support C3 inheritance on the Mac correctly. Rich extensions to primitive/value data-types for files, etc. Optimization for very large in-clauses. Database updates now infer table structure from class meta-data instead of leaning on database metadata when inserting (update and delete already do this). Bug fixes to the new boolean expression parser. Fixes to complex inheritance in RDBMS data. Fix to sorting issues in older Perl 5.8. Bug fixes to boolean expressions with values which are non-UR objects Smarter query plans when the join table is variable (not supported in SQL, but in the API), leading to multiple database queries where necessary. 0.37 2012-02-03 Added a proper parser for generating Boolean Expressions from text strings. The object lister commands (UR::Object::Command::List) use it to process the --filter, and it can be used directly through the method UR::BoolExpr::resolve_for_string(). See the UR::BoolExpr pod for more info. Or-type Boolean Expressions now support -order, and can be the filter for iterators. Important Bugfixes: * Better error messages when a module fails to load properly during autoloading. * Class methods called on Set instances are dispatched to the proper class instead of called on the Set's members. * Values in an SQL in-clause are escaped using DBI's quote() method. 0.36 2012-01-05 Fix for 'like' clause's escape string on PostgreSQL Speed improvement for class initialization by normalizing metadata more efficiently and only calculating the cached data for property_meta_for_name() once. Workaround for a bug in Perl 5.8 involving sorters by avoiding method calls inside some sort subs Fully deprecate the old Subscription API in favor of the new Observer api UR::Value classes use UR::DataSource::Default and the normal loading mechanism. Previously, UR::Values used a special codepath to get loaded into memory Add a json perspective for available views Allow descending sorts in order-by. For example: my @o = Some::Class->get(prop => 'value', -order => ['field1','-field2'] To get all objects where prop is equal to the string 'value', first sorted by field1 in ascending order, then by field2 in descending order Standardize sorting results on columns with NULLs by having NULL/undef always appears at the end for ascending sorts. Previously, the order depended on the data source's behavior. Oracle and PostgreSQL put them at the end, while MySQL, SQLite and cached get()s put them at the beginning. Fix exit code for 'ur test run' when the --lsf arg is used. It used always return a false value (1). Now it returns true (0) if all tests pass, and false (1) if any one test fails. UR::Object now implements the messaging API that used to be in Command (error_message, dump_error_messages, etc). The old messaging API is now deprecated. 0.35 2011-10-28 Queries with the -recurse option are suppored for all datasources, not just those that support recursive queries directly Make the object listers more user-friendly by implicitly putting '%' wildcards on either side of the user-supplied 'like' filter Update to the latest version of Getopt::Complete for command-line completion Object Set fixes (non-datasource expressable filters) Bugfixes for queries involving multiple joins to the same table with different join conditions Queries with -offset/-limit and -page are now supported. Query efficiency improvements: * id_by properties with a know data_type have special code in the bridging logic to handle them more efficiently * large in-clause testing uses a binary search instead of linear for cached objects * no longer indexing delegated properties results in fewer unnecessary queries during loading * remove unnecessary rule evaluations against loaded objects * When a query includes a filter or -hints for a calculated property, implicitly add its calculate_from properties to the -hints list * Rules in the query cache are always normalized, which makes many lookups faster * Fix a bug where rules in the query cache related to in-clause queries were malformed, resulting in fewer queries to the data source Command module fixes: * running with --help no longer emits error messages about other missing params * Help output only lists properties that are is_input or is_param Deleted objects hanging around as UR::DeletedRefs are recycled if the original object gets re-created 0.34 2011-07-26 New class (Command::SubCommandFactory) which can act as a factory for a tree of sub-commands Remove the distinction between older and newer versions of DBD::SQLite installed on the system. If you have SQLite databases (including MetaDBs) with names like "*sqlite3n*", they will need to be renamed to "*sqlite3*". Make the tests emit fewer messages to the terminal when run in the harness; improve coverage on non-Intel/Linux systems. 0.33 2011-06-30 New environment variable (UR_DBI_SUMMARIZE_SQL) to help find query optimization targets View aspects for objects' primitive values use the appropriate UR::Value View classes Query engine remembers cases where a left join matches nothing, and skips asking the datasource on subsequent similar queries Committing a software transaction now performs the same data consistancy checking as the top-level transaction. Improved document auto-generation for Command classes Improved SQLite Data Source schema introspection Updated database handling for Pg and mysql table case sensitivity UR's developer tools (ur command-line tool) can operate on non-standard source tree layouts, and can be forced to operate on a namespace with a command-line option Support for using a chain of properties in queries ('a.b.c like' => $v) Set operations normalized: min, max, sum, count Set-to-set relaying is now correctly lazy Objects previously loaded from the database, and later deleted from the database, are now detected as deleted and handled as another type of change to be merged with in-memory changes. 0.32 (skipped) 0.31 (skipped) 0.30 2011-03-07 re-package 0.29 with versions correctly set 0.29 2011-03-07 query/iteration engine now solves n+1 in the one-to-many case as well as many-to-one query optimization where the join table is variable across rows in a single resultset automated manual page creation for commands reduced deps (removed UR::Time) 0.28 2011-01-23 fix to the installer which caused a failure during docs generation improvements to man page generation 0.27 2011-01-22 updated build process autogenerates man pages 0.26 2011-01-16 yet another refactoring to ensure VERSION appears on all modules fixes for tests which fail only in the install harness 0.25 2011-01-15 Updated docs. 0.24 2011-01-15 Updated deps to compile fully on a new OSX installation (requires XCode). 0.22 2011-01-12 VERSION refactoring for cleaner uploads 0.20 2011-01-11 faster compile (<.5s) faster object creation faster install documentation polish 0.19 2010-12-24 faster compile faster query cache resolution leaner meta-data less build deps, build dep fixes hideable commands fixes for newer sqlite API revamped UR::BoolExpr API new command tree 0.18 2010-12-10 Bugfix for queries involving subclasses without tables Preliminary support for building debian packages Bugfixes for queries with the 'in' and 'not in' operators Object cache indexing sped up by replacing regexes with direct string comparisons 0.17 2010-11-10 Fixed bug with default datasources dumping debug info during queries. Deprecated old parts of the UR::Object API. Bugfixes for MySQL data sources with handling of between and like operators, and table/column name case sensitivity MySQL data sources will complain if the 'lower_case_table_names' setting is not set to 1 Bugfixes for FileMux data sources to return objects from iterators in correct sorted order File data sources remember their file offsets more often to improve seeking Bugfixes for handling is_many values passed in during create() New class for JSON-formatted Set views More consistent behavior during evaluation of BoolExprs with is_many values and undef/NULL values Bugfixes for handling observers during software transaction commit and rollback Addition of a new UR::Change type (external_change) to track non-UR entities that need undo-ing during a rollback 0.16 2010-09-27 File datasources build an on-the-fly index to improve its ability to seek within the file Initial support for classes to supply custom logic for loading data Compile-time speed improvements Bug fixes for SQL generation with indirect properties, and the object cache pruner 0.15 2010-08-03 Improved 'ur update classes' interaction with MySQL databases Integration with Getopt::Complete for bash command-line tab completion 0.14 2010-07-26 Metadata about data source entities (tables, columns, etc) is autodiscovered within commit() if it doesn't already exist in the MetaDB The new View API now has working default toolkits for HTML, Text, XML and XSL. The old Viewer API has been removed. Smarter property merging when the Context reloads an already cached object and the data in the data source has changed Added a built-in 'product' calculation property type Calculated properties can now be memoized subclassify_by for an abstract class can now be a regular, indirect or calculated property New environment variable UR_CONTEXT_MONITOR_QUERY for printing Context/query info to stdout SQLite data sources can initialize themselves even if the sqlite3 executable cannot be found Test harness improvements: --junit and --color options, control-C stops tests and reports results 'use lib' within an autoloaded module stays in effect after the module is loaded 0.13 2010-02-21 Circular foreign key constraints between tables are now handled smartly handled in UR::DataSource::RDBMS. New meta-property properties: id_class_by, order_by, specify_by. Updated autogenerated Command documentation. Formalized the __extend_namespace__ callback for dynamic class creation. New Command::DynamicSubCommands class makes command trees for a group of classes easy. The new view API is available. The old "viewer" API is still available in this release, but is deprecated. 0.12 2009-09-09 'ur test run' can now run tests in parallel and can submit tests as jobs to LSF Command modules now have support for Getopt::Complete for bash tab-completion Bugfixes related to saving objects to File data sources. Several more fixes for merging between database and in-memory objects. Property names beginning with an underscore are now handled properly during rule and object creation 0.11 2009-07-30 Fix bug in merge between database/in-memory data sets with changes. 0.10 2009-07-22 Updates to the UR::Object::Type MOP documentation. Other documentation cleanup and file cleanup. 0.9 2009-06-19 Additional build fixes. 0.8 2009-06-17 David's build fixes. 0.7 2009-06-10 Fix to build process: the distribution will work if you do not have Module::Install installed. 0.6 2009-06-07 Fixed to build process: actually install the "ur" executable. 0.5 2009-06-06 Updates to POD. Additional API updates to UR::Object. 0.4 2009-06-04 Updates to POD. Extensive API updates to UR::Object. 0.3 2009-05-29 Fixed memory leak in cache pruner, and added additional debugging environment variable. Additional laziness on file-based data-sources. Updated lots of POD. Switched to version numbers without zero padding! 0.02 2009-05-23 Cleanup of initial deployment issues. UR uses a non-default version of Class::Autouse. This is now a special file to prevent problems with the old version. Links to old DBIx::Class modules are now gone. Updated boolean expression API. 0.01 2009-05-07 First public release for Lambda Lounge language shootout. README000444023532023421 261212121654175 12503 0ustar00abrummetgsc000000000000UR-0.41UR is a Class Framework and Object/Relational Mapper (ORM) for Perl. After installing, run the "ur" command for a list of options. As a Class Framework, it starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and builds upon that with a more formal way to describe classes and their properties, object caching, and metadata about the classes and the ways they connect to each other. As an ORM, it aims to relieve the developer from having to think about the SQL behind any particular request, instead using the class structure and its metadata as a guide for where the data will be found. Behind the scenes, the RDBMS portion can handle JOINs (both INNER and OUTER) representing inheritance and indirect properties, multi-column primary and foreign keys, and iterators. It does its best to only query the database for information you've directly asked for, and to not query the database for something that has been loaded before. Oracle, SQLite, MySQL and PostgreSQL are all supported. Additionally, UR can use files or collections of files as if they were tables in a database, as well as internally handling the equivalent of an SQL join between two or more databases if that's what the query and class structure indicates. UR.pm contains more introductory POD documentation. UR::Manual has a short list of documentation you're likely to want to see next. META.yml000444023532023421 6362112121654175 13123 0ustar00abrummetgsc000000000000UR-0.41--- name: UR version: 0.41 author: - Anthony Brummett brummett@cpan.org - Scott Smith sakoht@cpan.org abstract: rich declarative transactional objects license: perl resources: license: http://dev.perl.org/licenses/ requires: Carp: '' Class::AutoloadCAN: 0.03 Class::Autouse: 2.0 Clone::PP: 1.02 DBD::SQLite: 1.14 DBI: 1.601 Data::Compare: 0.13 Data::UUID: 0.148 Date::Format: '' Devel::GlobalDestruction: '' File::Basename: 2.73 File::Path: '' File::Temp: '' FreezeThaw: 0.43 Getopt::Complete: 0.26 JSON: '' Lingua::EN::Inflect: 1.88 List::MoreUtils: '' MRO::Compat: '' Path::Class: '' Pod::Simple::HTML: 3.03 Pod::Simple::Text: 2.02 Sub::Install: 0.924 Sub::Name: 0.04 Sys::Hostname: 1.11 Test::Fork: '' Text::Diff: 0.35 Text::Glob: '' YAML: '' perl: v5.8.7 version: '' configure_requires: Module::Build: 0.340201 provides: Command: file: lib/Command.pm version: 0.41 Command::DynamicSubCommands: file: lib/Command/DynamicSubCommands.pm Command::Shell: file: lib/Command/Shell.pm Command::SubCommandFactory: file: lib/Command/SubCommandFactory.pm Command::Test: file: lib/Command/Test.pm Command::Test::Echo: file: lib/Command/Test/Echo.pm Command::Test::Tree1: file: lib/Command/Test/Tree1.pm Command::Test::Tree1::Echo1: file: lib/Command/Test/Tree1/Echo1.pm Command::Test::Tree1::Echo2: file: lib/Command/Test/Tree1/Echo2.pm Command::Tree: file: lib/Command/Tree.pm version: 0.41 Command::V1: file: lib/Command/V1.pm version: 0.41 Command::V2: file: lib/Command/V2.pm version: 0.41 DB: file: lib/Devel/callcount.pm Devel::callsfrom: file: lib/Devel/callcount.pm My::TAP::Parser::Iterator::Process::LSF: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::IteratorFactory::LSF: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Multiplexer: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Scheduler: file: lib/UR/Namespace/Command/Test/Run.pm My::TAP::Parser::Timer: file: lib/UR/Namespace/Command/Test/Run.pm UR: file: lib/UR.pm version: 0.41 UR::All: file: lib/UR/All.pm version: 0.41 UR::BoolExpr: file: lib/UR/BoolExpr.pm version: 0.41 UR::BoolExpr::BxParser: file: lib/UR/BoolExpr/BxParser.pm UR::BoolExpr::BxParser::Yapp::Driver: file: lib/UR/BoolExpr/BxParser.pm version: 1.05 UR::BoolExpr::Template: file: lib/UR/BoolExpr/Template.pm version: 0.41 UR::BoolExpr::Template::And: file: lib/UR/BoolExpr/Template/And.pm version: 0.41 UR::BoolExpr::Template::Composite: file: lib/UR/BoolExpr/Template/Composite.pm version: 0.41 UR::BoolExpr::Template::Or: file: lib/UR/BoolExpr/Template/Or.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison: file: lib/UR/BoolExpr/Template/PropertyComparison.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::Between: file: lib/UR/BoolExpr/Template/PropertyComparison/Between.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::Equals: file: lib/UR/BoolExpr/Template/PropertyComparison/Equals.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::False: file: lib/UR/BoolExpr/Template/PropertyComparison/False.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual: file: lib/UR/BoolExpr/Template/PropertyComparison/GreaterOrEqual.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::GreaterThan: file: lib/UR/BoolExpr/Template/PropertyComparison/GreaterThan.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::In: file: lib/UR/BoolExpr/Template/PropertyComparison/In.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::LessOrEqual: file: lib/UR/BoolExpr/Template/PropertyComparison/LessOrEqual.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::LessThan: file: lib/UR/BoolExpr/Template/PropertyComparison/LessThan.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::Like: file: lib/UR/BoolExpr/Template/PropertyComparison/Like.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::Matches: file: lib/UR/BoolExpr/Template/PropertyComparison/Matches.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::NotEqual: file: lib/UR/BoolExpr/Template/PropertyComparison/NotEqual.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::NotIn: file: lib/UR/BoolExpr/Template/PropertyComparison/NotIn.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::NotLike: file: lib/UR/BoolExpr/Template/PropertyComparison/NotLike.pm version: 0.41 UR::BoolExpr::Template::PropertyComparison::True: file: lib/UR/BoolExpr/Template/PropertyComparison/True.pm version: 0.41 UR::BoolExpr::Util: file: lib/UR/BoolExpr/Util.pm version: 0.41 UR::Change: file: lib/UR/Change.pm version: 0.41 UR::Context: file: lib/UR/Context.pm version: 0.41 UR::Context::DefaultRoot: file: lib/UR/Context/DefaultRoot.pm version: 0.41 UR::Context::LoadingIterator: file: lib/UR/Context/LoadingIterator.pm version: 0.41 UR::Context::ObjectFabricator: file: lib/UR/Context/ObjectFabricator.pm version: 0.41 UR::Context::Process: file: lib/UR/Context/Process.pm version: 0.41 UR::Context::Root: file: lib/UR/Context/Root.pm version: 0.41 UR::Context::Transaction: file: lib/UR/Context/Transaction.pm version: 0.41 UR::DBI: file: lib/UR/DBI.pm version: 0.41 UR::DBI::Report: file: lib/UR/DBI/Report.pm version: 0.41 UR::DBI::db: file: lib/UR/DBI.pm UR::DBI::st: file: lib/UR/DBI.pm UR::DataSource: file: lib/UR/DataSource.pm version: 0.41 UR::DataSource::CSV: file: lib/UR/DataSource/CSV.pm version: 0.41 UR::DataSource::Code: file: lib/UR/DataSource/Code.pm version: 0.41 UR::DataSource::Default: file: lib/UR/DataSource/Default.pm version: 0.41 UR::DataSource::File: file: lib/UR/DataSource/File.pm version: 0.41 UR::DataSource::FileMux: file: lib/UR/DataSource/FileMux.pm version: 0.41 UR::DataSource::Filesystem: file: lib/UR/DataSource/Filesystem.pm version: 0.41 UR::DataSource::Meta: file: lib/UR/DataSource/Meta.pm version: 0.41 UR::DataSource::MySQL: file: lib/UR/DataSource/MySQL.pm version: 0.41 UR::DataSource::Oracle: file: lib/UR/DataSource/Oracle.pm version: 0.41 UR::DataSource::Pg: file: lib/UR/DataSource/Pg.pm version: 0.41 UR::DataSource::QueryPlan: file: lib/UR/DataSource/QueryPlan.pm version: 0.41 UR::DataSource::RDBMS: file: lib/UR/DataSource/RDBMS.pm version: 0.41 UR::DataSource::RDBMS::BitmapIndex: file: lib/UR/DataSource/RDBMS/BitmapIndex.pm version: 0.41 UR::DataSource::RDBMS::Entity: file: lib/UR/DataSource/RDBMS/Entity.pm version: 0.41 UR::DataSource::RDBMS::FkConstraint: file: lib/UR/DataSource/RDBMS/FkConstraint.pm version: 0.41 UR::DataSource::RDBMS::FkConstraintColumn: file: lib/UR/DataSource/RDBMS/FkConstraintColumn.pm version: 0.41 UR::DataSource::RDBMS::PkConstraintColumn: file: lib/UR/DataSource/RDBMS/PkConstraintColumn.pm version: 0.41 UR::DataSource::RDBMS::Table: file: lib/UR/DataSource/RDBMS/Table.pm version: 0.41 UR::DataSource::RDBMS::Table::View::Default::Text: file: lib/UR/DataSource/RDBMS/Table/View/Default/Text.pm version: 0.41 UR::DataSource::RDBMS::TableColumn: file: lib/UR/DataSource/RDBMS/TableColumn.pm version: 0.41 UR::DataSource::RDBMS::TableColumn::View::Default::Text: file: lib/UR/DataSource/RDBMS/TableColumn/View/Default/Text.pm version: 0.41 UR::DataSource::RDBMS::UniqueConstraintColumn: file: lib/UR/DataSource/RDBMS/UniqueConstraintColumn.pm version: 0.41 UR::DataSource::SQLite: file: lib/UR/DataSource/SQLite.pm version: 0.41 UR::DataSource::ValueDomain: file: lib/UR/DataSource/ValueDomain.pm version: 0.41 UR::Debug: file: lib/UR/Debug.pm version: 0.41 UR::DeletedRef: file: lib/UR/DeletedRef.pm version: 0.41 UR::Doc::Pod2Html: file: lib/UR/Doc/Pod2Html.pm version: 0.41 UR::Doc::Section: file: lib/UR/Doc/Section.pm version: 0.41 UR::Doc::Writer: file: lib/UR/Doc/Writer.pm version: 0.41 UR::Doc::Writer::Html: file: lib/UR/Doc/Writer/Html.pm version: 0.41 UR::Doc::Writer::Pod: file: lib/UR/Doc/Writer/Pod.pm version: 0.41 UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES: file: lib/UR/Env/UR_COMMAND_DUMP_STATUS_MESSAGES.pm version: 0.41 UR::Env::UR_CONTEXT_BASE: file: lib/UR/Env/UR_CONTEXT_BASE.pm version: 0.41 UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER: file: lib/UR/Env/UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm version: 0.41 UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER: file: lib/UR/Env/UR_CONTEXT_CACHE_SIZE_LOWWATER.pm version: 0.41 UR::Env::UR_CONTEXT_LIBS: file: lib/UR/Env/UR_USED_LIBS.pm version: 0.41 UR::Env::UR_CONTEXT_MONITOR_QUERY: file: lib/UR/Env/UR_CONTEXT_MONITOR_QUERY.pm version: 0.41 UR::Env::UR_CONTEXT_ROOT: file: lib/UR/Env/UR_CONTEXT_ROOT.pm version: 0.41 UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT: file: lib/UR/Env/UR_DBI_DUMP_STACK_ON_CONNECT.pm version: 0.41 UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_CALLSTACK.pm version: 0.41 UR::Env::UR_DBI_EXPLAIN_SQL_IF: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_IF.pm version: 0.41 UR::Env::UR_DBI_EXPLAIN_SQL_MATCH: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_MATCH.pm version: 0.41 UR::Env::UR_DBI_EXPLAIN_SQL_SLOW: file: lib/UR/Env/UR_DBI_EXPLAIN_SQL_SLOW.pm version: 0.41 UR::Env::UR_DBI_MONITOR_DML: file: lib/UR/Env/UR_DBI_MONITOR_DML.pm version: 0.41 UR::Env::UR_DBI_MONITOR_EVERY_FETCH: file: lib/UR/Env/UR_DBI_MONITOR_EVERY_FETCH.pm version: 0.41 UR::Env::UR_DBI_MONITOR_SQL: file: lib/UR/Env/UR_DBI_MONITOR_SQL.pm version: 0.41 UR::Env::UR_DBI_NO_COMMIT: file: lib/UR/Env/UR_DBI_NO_COMMIT.pm version: 0.41 UR::Env::UR_DBI_SUMMARIZE_SQL: file: lib/UR/Env/UR_DBI_SUMMARIZE_SQL.pm version: 0.41 UR::Env::UR_DEBUG_OBJECT_PRUNING: file: lib/UR/Env/UR_DEBUG_OBJECT_PRUNING.pm version: 0.41 UR::Env::UR_DEBUG_OBJECT_RELEASE: file: lib/UR/Env/UR_DEBUG_OBJECT_RELEASE.pm version: 0.41 UR::Env::UR_IGNORE: file: lib/UR/Env/UR_IGNORE.pm version: 0.41 UR::Env::UR_NO_REQUIRE_USER_VERIFY: file: lib/UR/Env/UR_NO_REQUIRE_USER_VERIFY.pm version: 0.41 UR::Env::UR_NR_CPU: file: lib/UR/Env/UR_NR_CPU.pm version: 0.41 UR::Env::UR_RUN_LONG_TESTS: file: lib/UR/Env/UR_RUN_LONG_TESTS.pm version: 0.41 UR::Env::UR_STACK_DUMP_ON_DIE: file: lib/UR/Env/UR_STACK_DUMP_ON_DIE.pm version: 0.41 UR::Env::UR_STACK_DUMP_ON_WARN: file: lib/UR/Env/UR_STACK_DUMP_ON_WARN.pm version: 0.41 UR::Env::UR_TEST_FILLDB: file: lib/UR/Env/UR_TEST_FILLDB.pm version: 0.41 UR::Env::UR_TEST_QUIET: file: lib/UR/Env/UR_TEST_QUIET.pm version: 0.41 UR::Env::UR_USED_MODS: file: lib/UR/Env/UR_USED_MODS.pm version: 0.41 UR::Env::UR_USE_ANY: file: lib/UR/Env/UR_USE_ANY.pm version: 0.41 UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS: file: lib/UR/Env/UR_USE_DUMMY_AUTOGENERATED_IDS.pm version: 0.41 UR::Exit: file: lib/UR/Exit.pm version: 0.41 UR::ModuleBase: file: lib/UR/ModuleBase.pm version: 0.41 UR::ModuleBase::Message: file: lib/UR/ObjectDeprecated.pm UR::ModuleBuild: file: lib/UR/ModuleBuild.pm UR::ModuleConfig: file: lib/UR/ModuleConfig.pm version: 0.41 UR::ModuleLoader: file: lib/UR/ModuleLoader.pm version: 0.41 UR::Namespace: file: lib/UR/Namespace.pm version: 0.41 UR::Namespace::Command: file: lib/UR/Namespace/Command.pm version: 0.41 UR::Namespace::Command::Base: file: lib/UR/Namespace/Command/Base.pm version: 0.41 UR::Namespace::Command::Define: file: lib/UR/Namespace/Command/Define.pm version: 0.41 UR::Namespace::Command::Define::Class: file: lib/UR/Namespace/Command/Define/Class.pm version: 0.41 UR::Namespace::Command::Define::Datasource: file: lib/UR/Namespace/Command/Define/Datasource.pm version: 0.41 UR::Namespace::Command::Define::Datasource::File: file: lib/UR/Namespace/Command/Define/Datasource/File.pm version: 0.41 UR::Namespace::Command::Define::Datasource::Mysql: file: lib/UR/Namespace/Command/Define/Datasource/Mysql.pm version: 0.41 UR::Namespace::Command::Define::Datasource::Oracle: file: lib/UR/Namespace/Command/Define/Datasource/Oracle.pm version: 0.41 UR::Namespace::Command::Define::Datasource::Pg: file: lib/UR/Namespace/Command/Define/Datasource/Pg.pm version: 0.41 UR::Namespace::Command::Define::Datasource::Rdbms: file: lib/UR/Namespace/Command/Define/Datasource/Rdbms.pm version: 0.41 UR::Namespace::Command::Define::Datasource::RdbmsWithAuth: file: lib/UR/Namespace/Command/Define/Datasource/RdbmsWithAuth.pm version: 0.41 UR::Namespace::Command::Define::Datasource::Sqlite: file: lib/UR/Namespace/Command/Define/Datasource/Sqlite.pm version: 0.41 UR::Namespace::Command::Define::Db: file: lib/UR/Namespace/Command/Define/Db.pm version: 0.41 UR::Namespace::Command::Define::Namespace: file: lib/UR/Namespace/Command/Define/Namespace.pm version: 0.41 UR::Namespace::Command::Init: file: lib/UR/Namespace/Command/Init.pm version: 0.41 UR::Namespace::Command::List: file: lib/UR/Namespace/Command/List.pm version: 0.41 UR::Namespace::Command::List::Classes: file: lib/UR/Namespace/Command/List/Classes.pm version: 0.41 UR::Namespace::Command::List::Modules: file: lib/UR/Namespace/Command/List/Modules.pm version: 0.41 UR::Namespace::Command::List::Objects: file: lib/UR/Namespace/Command/List/Objects.pm version: 0.41 UR::Namespace::Command::Old: file: lib/UR/Namespace/Command/Old.pm version: 0.41 UR::Namespace::Command::Old::DiffRewrite: file: lib/UR/Namespace/Command/Old/DiffRewrite.pm version: 0.41 UR::Namespace::Command::Old::DiffUpdate: file: lib/UR/Namespace/Command/Old/DiffUpdate.pm version: 0.41 UR::Namespace::Command::Old::ExportDbicClasses: file: lib/UR/Namespace/Command/Old/ExportDbicClasses.pm version: 0.41 UR::Namespace::Command::Old::Info: file: lib/UR/Namespace/Command/Old/Info.pm version: 0.41 UR::Namespace::Command::Old::Redescribe: file: lib/UR/Namespace/Command/Old/Redescribe.pm version: 0.41 UR::Namespace::Command::RunsOnModulesInTree: file: lib/UR/Namespace/Command/RunsOnModulesInTree.pm version: 0.41 UR::Namespace::Command::Show: file: lib/UR/Namespace/Command/Show.pm UR::Namespace::Command::Show::Properties: file: lib/UR/Namespace/Command/Show/Properties.pm version: 0.41 UR::Namespace::Command::Show::Schema: file: lib/UR/Namespace/Command/Show/Schema.pm UR::Namespace::Command::Show::Subclasses: file: lib/UR/Namespace/Command/Show/Subclasses.pm UR::Namespace::Command::Sys: file: lib/UR/Namespace/Command/Sys.pm version: 0.41 UR::Namespace::Command::Sys::ClassBrowser: file: lib/UR/Namespace/Command/Sys/ClassBrowser.pm version: 0.41 UR::Namespace::Command::Test: file: lib/UR/Namespace/Command/Test.pm version: 0.41 UR::Namespace::Command::Test::Callcount: file: lib/UR/Namespace/Command/Test/Callcount.pm version: 0.41 UR::Namespace::Command::Test::Callcount::List: file: lib/UR/Namespace/Command/Test/Callcount/List.pm version: 0.41 UR::Namespace::Command::Test::Compile: file: lib/UR/Namespace/Command/Test/Compile.pm version: 0.41 UR::Namespace::Command::Test::Eval: file: lib/UR/Namespace/Command/Test/Eval.pm version: 0.41 UR::Namespace::Command::Test::Run: file: lib/UR/Namespace/Command/Test/Run.pm version: 0.41 UR::Namespace::Command::Test::TrackObjectRelease: file: lib/UR/Namespace/Command/Test/TrackObjectRelease.pm version: 0.41 UR::Namespace::Command::Test::Use: file: lib/UR/Namespace/Command/Test/Use.pm version: 0.41 UR::Namespace::Command::Test::Window: file: lib/UR/Namespace/Command/Test/Window.pm version: 0.41 UR::Namespace::Command::Test::Window::Tk: file: lib/UR/Namespace/Command/Test/Window.pm UR::Namespace::Command::Update: file: lib/UR/Namespace/Command/Update.pm version: 0.41 UR::Namespace::Command::Update::ClassDiagram: file: lib/UR/Namespace/Command/Update/ClassDiagram.pm version: 0.41 UR::Namespace::Command::Update::ClassesFromDb: file: lib/UR/Namespace/Command/Update/ClassesFromDb.pm version: 0.41 UR::Namespace::Command::Update::Doc: file: lib/UR/Namespace/Command/Update/Doc.pm version: 0.41 UR::Namespace::Command::Update::Pod: file: lib/UR/Namespace/Command/Update/Pod.pm version: 0.41 UR::Namespace::Command::Update::RenameClass: file: lib/UR/Namespace/Command/Update/RenameClass.pm version: 0.41 UR::Namespace::Command::Update::RewriteClassHeader: file: lib/UR/Namespace/Command/Update/RewriteClassHeader.pm version: 0.41 UR::Namespace::Command::Update::SchemaDiagram: file: lib/UR/Namespace/Command/Update/SchemaDiagram.pm version: 0.41 UR::Namespace::Command::Update::TabCompletionSpec: file: lib/UR/Namespace/Command/Update/TabCompletionSpec.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp::Base: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp/Base.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp::Class: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp/Class.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp::File: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp/File.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp::Index: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp/Index.pm version: 0.41 UR::Namespace::View::SchemaBrowser::CgiApp::Schema: file: lib/UR/Namespace/View/SchemaBrowser/CgiApp/Schema.pm version: 0.41 UR::Object: file: lib/UR/Object.pm version: 0.41 UR::Object::Accessorized: file: lib/UR/Object/Accessorized.pm version: 0.41 UR::Object::Command::FetchAndDo: file: lib/UR/Object/Command/FetchAndDo.pm version: 0.41 UR::Object::Command::List: file: lib/UR/Object/Command/List.pm version: 0.41 UR::Object::Command::List::Csv: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Html: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Newtext: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Pretty: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Style: file: lib/UR/Object/Command/List/Style.pm version: 0.41 UR::Object::Command::List::Text: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Tsv: file: lib/UR/Object/Command/List/Style.pm UR::Object::Command::List::Xml: file: lib/UR/Object/Command/List/Style.pm UR::Object::Ghost: file: lib/UR/Object/Ghost.pm version: 0.41 UR::Object::Index: file: lib/UR/Object/Index.pm version: 0.41 UR::Object::Iterator: file: lib/UR/Object/Iterator.pm version: 0.41 UR::Object::Join: file: lib/UR/Object/Join.pm version: 0.41 UR::Object::Property: file: lib/UR/Object/Property.pm version: 0.41 UR::Object::Property::View::Default::Text: file: lib/UR/Object/Property/View/Default/Text.pm version: 0.41 UR::Object::Property::View::DescriptionLineItem::Text: file: lib/UR/Object/Property/View/DescriptionLineItem/Text.pm version: 0.41 UR::Object::Property::View::ReferenceDescription::Text: file: lib/UR/Object/Property/View/ReferenceDescription/Text.pm version: 0.41 UR::Object::Set: file: lib/UR/Object/Set.pm version: 0.41 UR::Object::Set::View::Default::Html: file: lib/UR/Object/Set/View/Default/Html.pm version: 0.41 UR::Object::Set::View::Default::Json: file: lib/UR/Object/Set/View/Default/Json.pm version: 0.41 UR::Object::Set::View::Default::Text: file: lib/UR/Object/Set/View/Default/Text.pm version: 0.41 UR::Object::Set::View::Default::Xml: file: lib/UR/Object/Set/View/Default/Xml.pm version: 0.41 UR::Object::Tag: file: lib/UR/Object/Tag.pm version: 0.41 UR::Object::Type: file: lib/UR/Object/Type.pm version: 0.41 UR::Object::Type::AccessorWriter: file: lib/UR/Object/Type/AccessorWriter.pm UR::Object::Type::AccessorWriter::Product: file: lib/UR/Object/Type/AccessorWriter/Product.pm version: 0.41 UR::Object::Type::AccessorWriter::Sum: file: lib/UR/Object/Type/AccessorWriter/Sum.pm version: 0.41 UR::Object::Type::Initializer: file: lib/UR/Object/Type/Initializer.pm UR::Object::Type::ModuleWriter: file: lib/UR/Object/Type/ModuleWriter.pm UR::Object::Type::View::AvailableViews::Json: file: lib/UR/Object/Type/View/AvailableViews/Json.pm version: 0.41 UR::Object::Type::View::AvailableViews::Xml: file: lib/UR/Object/Type/View/AvailableViews/Xml.pm version: 0.41 UR::Object::Type::View::Default::Text: file: lib/UR/Object/Type/View/Default/Text.pm version: 0.41 UR::Object::Type::View::Default::Xml: file: lib/UR/Object/Type/View/Default/Xml.pm version: 0.41 UR::Object::Value: file: lib/UR/Object/Value.pm version: 0.41 UR::Object::View: file: lib/UR/Object/View.pm version: 0.41 UR::Object::View::Aspect: file: lib/UR/Object/View/Aspect.pm version: 0.41 UR::Object::View::Default::Gtk: file: lib/UR/Object/View/Default/Gtk.pm version: 0.41 UR::Object::View::Default::Gtk2: file: lib/UR/Object/View/Default/Gtk2.pm version: 0.41 UR::Object::View::Default::Html: file: lib/UR/Object/View/Default/Html.pm version: 0.41 UR::Object::View::Default::Json: file: lib/UR/Object/View/Default/Json.pm version: 0.41 UR::Object::View::Default::Text: file: lib/UR/Object/View/Default/Text.pm version: 0.41 UR::Object::View::Default::Xml: file: lib/UR/Object/View/Default/Xml.pm version: 0.41 UR::Object::View::Default::Xsl: file: lib/UR/Object/View/Default/Xsl.pm version: 0.41 UR::Object::View::Lister::Text: file: lib/UR/Object/View/Lister/Text.pm version: 0.41 UR::Object::View::Static::Html: file: lib/UR/Object/View/Static/Html.pm version: 0.41 UR::Object::View::Toolkit: file: lib/UR/Object/View/Toolkit.pm version: 0.41 UR::Object::View::Toolkit::Text: file: lib/UR/Object/View/Toolkit/Text.pm version: 0.41 UR::Observer: file: lib/UR/Observer.pm version: 0.41 UR::Service::JsonRpcServer: file: lib/UR/Service/JsonRpcServer.pm version: 0.41 UR::Service::RPC::Executer: file: lib/UR/Service/RPC/Executer.pm version: 0.41 UR::Service::RPC::Message: file: lib/UR/Service/RPC/Message.pm version: 0.41 UR::Service::RPC::Server: file: lib/UR/Service/RPC/Server.pm version: 0.41 UR::Service::RPC::TcpConnectionListener: file: lib/UR/Service/RPC/TcpConnectionListener.pm version: 0.41 UR::Singleton: file: lib/UR/Singleton.pm version: 0.41 UR::Test: file: lib/UR/Test.pm version: 0.41 UR::Util: file: lib/UR/Util.pm version: 0.41 UR::Value: file: lib/UR/Value.pm version: 0.41 UR::Value::ARRAY: file: lib/UR/Value/ARRAY.pm version: 0.41 UR::Value::Blob: file: lib/UR/Value/Blob.pm version: 0.41 UR::Value::Boolean: file: lib/UR/Value/Boolean.pm version: 0.41 UR::Value::Boolean::View::Default::Text: file: lib/UR/Value/Boolean/View/Default/Text.pm version: 0.41 UR::Value::CODE: file: lib/UR/Value/CODE.pm version: 0.41 UR::Value::CSV: file: lib/UR/Value/CSV.pm version: 0.41 UR::Value::DateTime: file: lib/UR/Value/DateTime.pm version: 0.41 UR::Value::Decimal: file: lib/UR/Value/Decimal.pm version: 0.41 UR::Value::DirectoryPath: file: lib/UR/Value/DirectoryPath.pm version: 0.41 UR::Value::FOF: file: lib/UR/Value/FOF.pm version: 0.41 UR::Value::FilePath: file: lib/UR/Value/FilePath.pm version: 0.41 UR::Value::FilesystemPath: file: lib/UR/Value/FilesystemPath.pm version: 0.41 UR::Value::Float: file: lib/UR/Value/Float.pm version: 0.41 UR::Value::GLOB: file: lib/UR/Value/GLOB.pm version: 0.41 UR::Value::HASH: file: lib/UR/Value/HASH.pm version: 0.41 UR::Value::Integer: file: lib/UR/Value/Integer.pm version: 0.41 UR::Value::Iterator: file: lib/UR/Value/Iterator.pm version: 0.41 UR::Value::Number: file: lib/UR/Value/Number.pm version: 0.41 UR::Value::PerlReference: file: lib/UR/Value/PerlReference.pm version: 0.41 UR::Value::REF: file: lib/UR/Value/REF.pm version: 0.41 UR::Value::SCALAR: file: lib/UR/Value/SCALAR.pm version: 0.41 UR::Value::Set: file: lib/UR/Value/Set.pm version: 0.41 UR::Value::SloppyPrimitive: file: lib/UR/Value/SloppyPrimitive.pm version: 0.41 UR::Value::String: file: lib/UR/Value/String.pm version: 0.41 UR::Value::Text: file: lib/UR/Value/Text.pm version: 0.41 UR::Value::Timestamp: file: lib/UR/Value/Timestamp.pm version: 0.41 UR::Value::URL: file: lib/UR/Value/URL.pm version: 0.41 UR::Value::View::Default::Html: file: lib/UR/Value/View/Default/Html.pm UR::Value::View::Default::Json: file: lib/UR/Value/View/Default/Json.pm UR::Value::View::Default::Text: file: lib/UR/Value/View/Default/Text.pm UR::Value::View::Default::Xml: file: lib/UR/Value/View/Default/Xml.pm UR::Vocabulary: file: lib/UR/Vocabulary.pm version: 0.41 above: file: lib/above.pm version: 0.02 class_name: file: lib/UR/Object/Type/Initializer.pm version: 2 generated_by: Module::Build version 0.340201 meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: 1.4 pod000755023532023421 012121654175 12247 5ustar00abrummetgsc000000000000UR-0.41ur-old-diff-rewrite.pod000444023532023421 57212121654172 16660 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old diff-rewrite - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur old diff-rewrite version 0.29. =head1 SYNOPSIS ur old diff-rewrite (no execute or sub commands implemented) =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-define-class.pod000444023532023421 130312121654172 16063 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define class - Add one or more classes to the current namespace =head1 VERSION This document describes ur define class version 0.29. =head1 SYNOPSIS ur define class --extends=? [NAMES] $ cd Acme $ ur define class Animal Vegetable Mineral A Acme::Animal A Acme::Vegetable A Acme::Mineral $ ur define class Dog Cat Bird --extends Animal A Acme::Dog A Acme::Cat A Acme::Bird =head1 REQUIRED ARGUMENTS =over =item extends The base class. Defaults to UR::Object. Default value 'UR::Object' if not specified =back =head1 OPTIONAL ARGUMENTS =over =item NAMES (undocumented) =back =head1 DESCRIPTION: Add one or more classes to the current namespace =cut ur-old.pod000444023532023421 226012121654172 14307 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old - define namespaces, data sources and classes =head1 VERSION This document describes ur old version 0.29. =head1 SUB-COMMANDS diff-rewrite (no execute or su... a command which operates on classes/modules in a UR namespace directory diff-update (no execute or su... a command which operates on classes/modules in a UR namespace directory export-dbic-classes [BARE-ARGS] [CLAS... Create or update a DBIx::Class class from an already existing UR class info [SUBJECT] Outputs description(s) of UR entities such as classes and tables to stdout redescribe [CLASSES-OR-MODULES] Outputs class description(s) formatted to the latest standard. =cut ur-test-eval.pod000444023532023421 123512121654172 15436 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test eval - Evaluate a string of Perl source =head1 VERSION This document describes ur test eval version 0.29. =head1 SYNOPSIS ur test eval [BARE-ARGS] ur test eval 'print "hello\n"' ur test eval 'print "hello\n"' 'print "goodbye\n"' ur test eval 'print "Testing in the " . \$self->namespace_name . " namespace.\n"' =head1 OPTIONAL ARGUMENTS =over =item BARE-ARGS (undocumented) =back =head1 DESCRIPTION: This command is for testing and debugging. It simply eval's the Perl source supplied on the command line, after using the current namespace. A $self object is in scope representing the current context. =cut ur-old-diff-update.pod000444023532023421 56712121654172 16465 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old diff-update - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur old diff-update version 0.29. =head1 SYNOPSIS ur old diff-update (no execute or sub commands implemented) =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-define-datasource-file.pod000444023532023421 165712121654172 20041 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource file - Add a file-based data source (not yet implemented) =head1 VERSION This document describes ur define datasource file version 0.29. =head1 SYNOPSIS ur define datasource file --server=? [--singleton] [--dsid=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item server I "server" attribute for this data source, such as a database name =item singleton I by default all data sources are singletons, but this can be turned off Default value 'true' if not specified =item nosingleton I Make singleton 'false' =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a file-based data source (not yet implemented) =cut ur-list-modules.pod000444023532023421 65712121654172 16142 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur list modules - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur list modules version 0.29. =head1 SYNOPSIS ur list modules [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-list.pod000444023532023421 111012121654172 14475 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur list - list objects, classes, modules =head1 VERSION This document describes ur list version 0.29. =head1 SUB-COMMANDS objects --subject-class-n... lists objects matching specified params classes [CLASSES-OR-MODULES] a command which operates on classes/modules in a UR namespace directory modules [CLASSES-OR-MODULES] a command which operates on classes/modules in a UR namespace directory =cut ur-update.pod000444023532023421 262612121654172 15021 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update - update parts of the source tree of a UR namespace =head1 VERSION This document describes ur update version 0.29. =head1 SUB-COMMANDS classes-from-db [--class-name=?] ... Update class definitions (and data dictionary cache) to reflect changes in the database schema. schema-diagram --file=? [--data-... Update an Umlet diagram based on the current schema class-diagram --file=? [--data-... Update an Umlet diagram based on the current class definitions pod [--input-path=?] ... generate man-page-like POD for a commands rename-class [--force] [CLASSE... Update::RewriteClassHeaders class descriptions headers to normalize manual changes. rewrite-class-header [--force] [CLASSE... Update::RewriteClassHeaders class descriptions headers to normalize manual changes. tab-completion-spec [--output=?] CLAS... Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =cut ur-define-namespace.pod000444023532023421 62212121654172 16675 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define namespace - create a new namespace tree and top-level module =head1 VERSION This document describes ur define namespace version 0.29. =head1 SYNOPSIS ur define namespace NSNAME =head1 REQUIRED ARGUMENTS =over =item NSNAME the name of the namespace, and first "word" in all classes =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-test-callcount-list.pod000444023532023421 467312121654173 17456 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test callcount list - Filter and list Callcount items =head1 VERSION This document describes ur test callcount list version 0.29. =head1 SYNOPSIS ur test callcount list --file=? --show=? [--csv-delimiter=?] [--filter=?] [--noheaders] [--style=?] =head1 REQUIRED ARGUMENTS =over =item file I Specify the .callcount file Default value '/dev/null' if not specified =item show Specify which columns to show, in order. Default value 'count,subname,subloc,callers' if not specified =back =head1 OPTIONAL ARGUMENTS =over =item csv-delimiter I For the csv output style, specify the field delimiter Default value ',' if not specified =item filter I Filter results based on the parameters. See below for how to. =item noheaders I Do not include headers Default value 'false' (--nonoheaders) if not specified =item nonoheaders I Make noheaders 'false' =item style I Style of the list: text (default), csv, pretty, html, xml Default value 'text' if not specified =back =head1 DESCRIPTION: Listing Styles: --------------- text - table like csv - comma separated values pretty - objects listed singly with color enhancements html - html table xml - xml document using elements Filtering: ---------- Create filter equations by combining filterable properties with operators and values. Combine and separate these 'equations' by commas. Use single quotes (') to contain values with spaces: name='genome institute' Use percent signs (%) as wild cards in like (~). Use backslash or single quotes to escape characters which have special meaning to the shell such as < > and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- lister-command --filter name=Bob --show id,name,address lister-command --filter name='something with space',employees>200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe Filterable Properties: ---------------------- callers (String): (undocumented) count (Integer): (undocumented) subloc (String): (undocumented) subname (String): (undocumented) =cut ur-test-run.pod000444023532023421 706312121654173 15321 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test run - Run the test suite against the source tree. =head1 VERSION This document describes ur test run version 0.29. =head1 SYNOPSIS ur test run [--color] [--junit] [--list] [--lsf] [--recurse] [--callcount] [--cover=?] [--cover-cvs-changes] [--cover-svk-changes] [--cover-svn-changes] [--coverage] [--inc=?[,?]] [--jobs=?] [--long] [--lsf-params=?] [--noisy] [--perl-opts=?] [--run-as-lsf-helper=?] [--script-opts=?] [--time=?] [BARE-ARGS] cd MyNamespace ur test run --recurse # run all tests in the namespace ur test run # runs all tests in the t/ directory under pwd ur test run t/mytest1.t My/Class.t # run specific tests ur test run -v -t --cover-svk-changes # run tests to cover latest svk updates ur test run -I ../some/path/ # Adds ../some/path to perl's @INC through -I ur test run --junit # writes test output in junit's xml format (consumable by Hudson integration system) =head1 REQUIRED ARGUMENTS =over =item color I Use TAP::Harness::Color to generate color output Default value 'false' (--nocolor) if not specified =item nocolor I Make color 'false' =item junit I Run all tests with junit style XML output. (requires TAP::Formatter::JUnit) =item nojunit I Make junit 'false' =item list I List the tests, but do not actually run them. =item nolist I Make list 'false' =item lsf I If true, tests will be submitted as jobs via bsub =item nolsf I Make lsf 'false' =item recurse I Run all .t files in the current directory, and in recursive subdirectories. =item norecurse I Make recurse 'false' =back =head1 OPTIONAL ARGUMENTS =over =item callcount I Count the number of calls to each subroutine/method =item nocallcount I Make callcount 'false' =item cover I Cover only this(these) modules =item cover-cvs-changes I Cover modules modified in cvs status =item nocover-cvs-changes I Make cover-cvs-changes 'false' =item cover-svk-changes I Cover modules modified in svk status =item nocover-svk-changes I Make cover-svk-changes 'false' =item cover-svn-changes I Cover modules modified in svn status =item nocover-svn-changes I Make cover-svn-changes 'false' =item coverage I Invoke Devel::Cover =item nocoverage I Make coverage 'false' =item inc I Additional paths for @INC, alias for -I =item jobs I How many tests to run in parallel Default value '1' if not specified =item long I Run tests including those flagged as long =item nolong I Make long 'false' =item lsf-params I Params passed to bsub while submitting jobs to lsf Default value '-q short -R select[type==LINUX64]' if not specified =item noisy I doesn't redirect stdout =item nonoisy I Make noisy 'false' =item perl-opts I Override options to the Perl interpreter when running the tests (-d:Profile, etc.) Default value '' if not specified =item run-as-lsf-helper I Used internally by the test harness =item script-opts I Override options to the test case when running the tests (--dump-sql --no-commit) Default value '' if not specified =item time I Write timelog sum to specified file =item BARE-ARGS (undocumented) =back =head1 DESCRIPTION: This command is like "prove" or "make test", running the test suite for the current namespace. =cut ur-define-datasource-pg.pod000444023532023421 221012121654173 17513 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource pg - Add a PostgreSQL data source to the current namespace. =head1 VERSION This document describes ur define datasource pg version 0.29. =head1 SYNOPSIS ur define datasource pg --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a PostgreSQL data source to the current namespace. =cut ur-define-db.pod000444023532023421 165112121654173 15352 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define db - add a data source to the current namespace =head1 VERSION This document describes ur define db version 0.29. =head1 SYNOPSIS ur define db URI NAME ur define db dbi:SQLite:/some/file.db Db1 ur define db me@dbi:mysql:myserver MainDb ur define db me@dbi:Oracle:someserver ProdDb ur define db me@dbi:Oracle:someserver~schemaname BigDb ur define db me@dbi:Pg:prod Db1 ur define db me@dbi:Pg:dev Testing::Db1 # alternate for "Testing" (arbitrary) context ur define db me@dbi:Pg:stage Staging::Db1 # alternate for "Staging" (arbitrary) context =head1 REQUIRED ARGUMENTS =over =item URI I a DBI connect string like dbi:mysql:someserver or user/passwd@dbi:Oracle:someserver~defaultns =item NAME I the name for this data source (used for class naming) Default value 'Db1' if not specified =back =head1 DESCRIPTION: add a data source to the current namespace =cut ur-update-pod.pod000444023532023421 175112121654173 15600 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update pod - generate man-page-like POD for a commands =head1 VERSION This document describes ur update pod version 0.29. (built on 2011-03-07 at 10:34:30) =head1 SYNOPSIS ur update pod [--input-path=?] [--output-path=?] EXECUTABLE-NAME CLASS-NAME TARGETS ur update pod -i ./lib -o ./pod ur UR::Namespace::Command =head1 REQUIRED ARGUMENTS =over =item EXECUTABLE-NAME I the name of the executable to document =item CLASS-NAME I the command class which maps to the executable =item TARGETS I specific classes to document (documents all unless specified) =back =head1 OPTIONAL ARGUMENTS =over =item input-path I optional location of the modules to document =item output-path I optional location to output .pod files =back =head1 DESCRIPTION: This tool generates POD documentation for each all of the commands in a tree for a given executable. This command must be run from within the namespace directory. =cut ur.pod000444023532023421 134112121654173 13533 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur - tools to create and maintain a ur class tree =head1 VERSION This document describes ur version 0.29. =head1 SUB-COMMANDS init NAMESPACE [DB] create a new ur app with default classes in place define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging =cut ur-init.pod000444023532023421 76012121654173 14460 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur init - create a new ur app with default classes in place =head1 VERSION This document describes ur init version 0.29. =head1 SYNOPSIS ur init NAMESPACE [DB] =head1 REQUIRED ARGUMENTS =over =item NAMESPACE I the name of the namespace/app to create =back =head1 OPTIONAL ARGUMENTS =over =item DB I the (optional) DBI connection string for the primary data source =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-test.pod000444023532023421 245612121654173 14520 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test - tools for testing and debugging =head1 VERSION This document describes ur test version 0.29. =head1 SUB-COMMANDS callcount ... Collect the data from a prior 'ur test run --callcount' run into a single output file compile [CLASSES-OR-MODULES] Attempts to compile each module in the namespace in its own process. eval [BARE-ARGS] Evaluate a string of Perl source run [--color] [--juni... Run the test suite against the source tree. track-object-release --file=? Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks use [--exec=?] [--sum... Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to @INC by any modules (bad!). window [SRC] repl tk window =cut ur-sys-class-browser.pod000444023532023421 52512121654173 17116 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur sys class-browser - Start a web server to browse through the class and database structures. =head1 VERSION This document describes ur sys class-browser version 0.29. =head1 SYNOPSIS ur sys class-browser =head1 DESCRIPTION: Start a web server to browse through the class and database structures. =cut ur-define-datasource-oracle.pod000444023532023421 221612121654173 20360 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource oracle - Add an Oracle data source to the current namespace. =head1 VERSION This document describes ur define datasource oracle version 0.29. =head1 SYNOPSIS ur define datasource oracle --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add an Oracle data source to the current namespace. =cut ur-list-classes.pod000444023532023421 65712121654174 16131 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur list classes - a command which operates on classes/modules in a UR namespace directory =head1 VERSION This document describes ur list classes version 0.29. =head1 SYNOPSIS ur list classes [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: a command which operates on classes/modules in a UR namespace directory =cut ur-update-rewrite-class-header.pod000444023532023421 161012121654174 21023 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update rewrite-class-header - Update::RewriteClassHeaders class descriptions headers to normalize manual changes. =head1 VERSION This document describes ur update rewrite-class-header version 0.29. =head1 SYNOPSIS ur update rewrite-class-header [--force] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item force I (undocumented) =item noforce I Make force 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. =cut ur-update-classes-from-db.pod000444023532023421 352112121654174 17775 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update classes-from-db - Update class definitions (and data dictionary cache) to reflect changes in the database schema. =head1 VERSION This document describes ur update classes-from-db version 0.29. =head1 SYNOPSIS ur update classes-from-db [--class-name=?] [--data-source=?] [--force-check-all-tables] [--force-rewrite-all-classes] [--table-name=?] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item class-name I Update only the specified classes. =item data-source I Limit updates to these data sources =item force-check-all-tables I By default we only look at tables with a new DDL time for changed database schema information. This explicitly (slowly) checks each table against our cache. =item noforce-check-all-tables I Make force-check-all-tables 'false' =item force-rewrite-all-classes I By default we only rewrite classes where there are database changes. Set this flag to rewrite all classes even where there are no schema changes. =item noforce-rewrite-all-classes I Make force-rewrite-all-classes 'false' =item table-name I Update the specified table. =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Reads from the data sources in the current working directory's namespace, and updates the local class tree. This hits the data dictionary for the remote database, and gets changes there first. Those changes are then used to mutate the class tree. If specific data sources are specified on the command-line, it will limit its database examination to just data in those data sources. This command will, however, always load ALL classes in the namespace when doing this update, to find classes which currently reference the updated table, or are connected to its class indirectly. =cut ur-update-tab-completion-spec.pod000444023532023421 125712121654174 20665 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update tab-completion-spec - Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =head1 VERSION This document describes ur update tab-completion-spec version 0.29. =head1 SYNOPSIS ur update tab-completion-spec [--output=?] CLASSNAME =head1 REQUIRED ARGUMENTS =over =item CLASSNAME I The base class to use as trunk of command tree, e.g. UR::Namespace::Command =back =head1 OPTIONAL ARGUMENTS =over =item output I Override output location of the opts spec file. =back =head1 DESCRIPTION: Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command. =cut ur-test-track-object-release.pod000444023532023421 147312121654174 20503 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test track-object-release - Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks =head1 VERSION This document describes ur test track-object-release version 0.29. =head1 SYNOPSIS ur test track-object-release --file=? ur test track-object-release --file /path/to/text.file > /path/to/results =head1 REQUIRED ARGUMENTS =over =item file I pathname of the input file =back =head1 DESCRIPTION: When a UR-based program is run with the UR_DEBUG_OBJECT_RELEASE environment variable set to 1, it will emit messages to STDERR describing the various stages of releasing an object. This command parses those messages and provides a report on objects which did not completely deallocate themselves, usually because of a reference being held. =cut ur-old-export-dbic-classes.pod000444023532023421 114312121654174 20161 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old export-dbic-classes - Create or update a DBIx::Class class from an already existing UR class =head1 VERSION This document describes ur old export-dbic-classes version 0.29. =head1 SYNOPSIS ur old export-dbic-classes [BARE-ARGS] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item BARE-ARGS (undocumented) =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Given one or more UR class names on the command line, this will create or update a DBIx::Class class. The files will appear under the DBIx directory in the namespace. =cut ur-define-datasource.pod000444023532023421 107412121654174 17117 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource - add a data source to the current namespace =head1 VERSION This document describes ur define datasource version 0.29. =head1 SYNOPSIS ur define datasource [file|mysql|oracle|pg|sqlite] ... =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: add a data source to the current namespace =cut ur-test-use.pod000444023532023421 303512121654174 15305 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test use - Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to @INC by any modules (bad!). =head1 VERSION This document describes ur test use version 0.29. =head1 SYNOPSIS ur test use [--exec=?] [--summarize-externals] [--verbose] [CLASSES-OR-MODULES] ur test use ur test use Some::Module Some::Other::Module ur test use ./Module.pm Other/Module.pm =head1 OPTIONAL ARGUMENTS =over =item exec I Execute the specified Perl _after_ using all of the modules. =item summarize-externals I List all modules used which are outside the namespace. =item nosummarize-externals I Make summarize-externals 'false' =item verbose I List each explicitly. =item noverbose I Make verbose 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Tests each module by "use"-ing it. Failures are reported individually. Successes are only reported individualy if the --verbose option is specified. A count of total successes/failures is returned as a summary in all cases. This command requires that the current working directory be under a namespace module. If no modules or class names are specified as parameters, it runs on all modules in the namespace. If modules or class names ARE listed, it will operate only on those. Words containing double-colons will be interpreted as absolute class names. All other words will be interpreted as relative file paths to modules. =cut ur-list-objects.pod000444023532023421 442412121654174 16141 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur list objects - lists objects matching specified params =head1 VERSION This document describes ur list objects version 0.29. =head1 SYNOPSIS ur list objects --subject-class-name=? [--csv-delimiter=?] [--filter=?] [--noheaders] [--show=?] [--style=?] =head1 REQUIRED ARGUMENTS =over =item subject-class-name I (undocumented) =back =head1 OPTIONAL ARGUMENTS =over =item csv-delimiter I For the csv output style, specify the field delimiter Default value ',' if not specified =item filter I Filter results based on the parameters. See below for how to. =item noheaders I Do not include headers Default value 'false' (--nonoheaders) if not specified =item nonoheaders I Make noheaders 'false' =item show I Specify which columns to show, in order. =item style I Style of the list: text (default), csv, pretty, html, xml Default value 'text' if not specified =back =head1 DESCRIPTION: Listing Styles: --------------- text - table like csv - comma separated values pretty - objects listed singly with color enhancements html - html table xml - xml document using elements Filtering: ---------- Create filter equations by combining filterable properties with operators and values. Combine and separate these 'equations' by commas. Use single quotes (') to contain values with spaces: name='genome institute' Use percent signs (%) as wild cards in like (~). Use backslash or single quotes to escape characters which have special meaning to the shell such as < > and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- lister-command --filter name=Bob --show id,name,address lister-command --filter name='something with space',employees>200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe Filterable Properties: ---------------------- Can't determine the list of filterable properties without a subject_class_name =cut ur-update-class-diagram.pod000444023532023421 332412121654175 17525 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update class-diagram - Update an Umlet diagram based on the current class definitions =head1 VERSION This document describes ur update class-diagram version 0.29. =head1 SYNOPSIS ur update class-diagram --file=? [--data-source=?] [--depth=?] [--include-ur-object] [--show-attributes] [--show-methods] [INITIAL-NAME] =head1 REQUIRED ARGUMENTS =over =item file I Pathname of the Umlet (.uxf) file =back =head1 OPTIONAL ARGUMENTS =over =item data-source I Which datasource to use =item depth I Max distance of related classes to include. Default is 1. 0 means show only the named class(es), -1 means to include everything =item include-ur-object I Include UR::Object and UR::Entity in the diagram (default = no) Default value 'false' (--noinclude-ur-object) if not specified =item noinclude-ur-object I Make include-ur-object 'false' =item show-attributes I Include class attributes in the diagram Default value 'true' if not specified =item noshow-attributes I Make show-attributes 'false' =item show-methods I Include methods in the diagram (not implemented yet Default value 'false' (--noshow-methods) if not specified =item noshow-methods I Make show-methods 'false' =item INITIAL-NAME (undocumented) =back =head1 DESCRIPTION: Creates a new Umlet diagram, or updates an existing diagram. Bare arguments are taken as class names to include in the diagram. Other classes may be included in the diagram based on their distance from the names classes and the --depth parameter. If an existing file is being updated, the position of existing elements will not change. =cut ur-update-rename-class.pod000444023532023421 156012121654175 17370 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update rename-class - Update::RewriteClassHeaders class descriptions headers to normalize manual changes. =head1 VERSION This document describes ur update rename-class version 0.29. =head1 SYNOPSIS ur update rename-class [--force] [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item force I (undocumented) =item noforce I Make force 'false' =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. =cut ur-sys.pod000444023532023421 46012121654175 14332 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur sys - service launchers =head1 VERSION This document describes ur sys version 0.29. =head1 SUB-COMMANDS class-browser Start a web server to browse through the class and database structures. =cut ur-describe.pod000444023532023421 75012121654175 15276 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur describe - show class properties, relationships, meta-data =head1 VERSION This document describes ur describe version 0.29. =head1 SYNOPSIS ur describe CLASSES-OR-MODULES ur describe UR::Object ur describe Acme::Order Acme::Product Acme::Order::LineItem =head1 REQUIRED ARGUMENTS =over =item CLASSES-OR-MODULES classes to describe by class name or module path =back =head1 DESCRIPTION: show class properties, relationships, meta-data =cut ur-define.pod000444023532023421 65112121654175 14750 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define - define namespaces, data sources and classes =head1 VERSION This document describes ur define version 0.29. =head1 SUB-COMMANDS namespace NSNAME create a new namespace tree and top-level module db URI NAME add a data source to the current namespace class --extends=? [NAMES] Add one or more classes to the current namespace =cut ur-old-redescribe.pod000444023532023421 64312121654175 16402 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old redescribe - Outputs class description(s) formatted to the latest standard. =head1 VERSION This document describes ur old redescribe version 0.29. =head1 SYNOPSIS ur old redescribe [CLASSES-OR-MODULES] =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: Outputs class description(s) formatted to the latest standard. =cut ur-update-schema-diagram.pod000444023532023421 232512121654175 17660 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur update schema-diagram - Update an Umlet diagram based on the current schema =head1 VERSION This document describes ur update schema-diagram version 0.29. =head1 SYNOPSIS ur update schema-diagram --file=? [--data-source=?] [--depth=?] [--show-columns] [INITIAL-NAME] =head1 REQUIRED ARGUMENTS =over =item file I Pathname of the Umlet (.uxf) file =back =head1 OPTIONAL ARGUMENTS =over =item data-source I Which datasource to use =item depth I Max distance of related tables to include. Default is 1. 0 means show only the named tables, -1 means to include everything =item show-columns I Include column names in the diagram Default value 'true' if not specified =item noshow-columns I Make show-columns 'false' =item INITIAL-NAME (undocumented) =back =head1 DESCRIPTION: Creates a new Umlet diagram, or updates an existing diagram. Bare arguments are taken as table names to include in the diagram. Other tables may be included in the diagram based on their distance from the names tables and the --depth parameter. If an existing file is being updated, the position of existing elements will not change. =cut ur-test-window.pod000444023532023421 46112121654175 16001 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test window - repl tk window =head1 VERSION This document describes ur test window version 0.29. =head1 SYNOPSIS ur test window [SRC] =head1 OPTIONAL ARGUMENTS =over =item SRC (undocumented) =back =head1 DESCRIPTION: !!! define help_detail() in module =cut ur-define-datasource-sqlite.pod000444023532023421 240712121654175 20420 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource sqlite - Add a SQLite data source to the current namespace. =head1 VERSION This document describes ur define datasource sqlite version 0.29. =head1 SYNOPSIS ur define datasource sqlite [--nosingleton] [--dsid=?] [--server=?] [DSNAME] cd Acme ur define datasource sqlite --dsname MyDB1 # writes Acme::DataSource::MyDB1 to work with Acme/DataSource/MyDB1.sqlite3 ur define datasource sqlite --dsname MyDB2 --server /var/lib/acmeapp/mydb2.sqlite3 # writes Acme::DataSource::MyDB2 to work with the specified sqlite file =head1 REQUIRED ARGUMENTS =over =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a SQLite data source to the current namespace. =cut ur-test-callcount.pod000444023532023421 45012121654175 16454 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test callcount - Collect the data from a prior 'ur test run --callcount' run into a single output file =head1 VERSION This document describes ur test callcount version 0.29. =head1 SUB-COMMANDS list --file=? --show=?... Filter and list Callcount items =cut ur-test-compile.pod000444023532023421 133512121654175 16143 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur test compile - Attempts to compile each module in the namespace in its own process. =head1 VERSION This document describes ur test compile version 0.29. =head1 SYNOPSIS ur test compile [CLASSES-OR-MODULES] ur test complie ur test compile Some::Module Some::Other::Module ur test complile Some/Module.pm Some/Other/Mod*.pm =head1 OPTIONAL ARGUMENTS =over =item CLASSES-OR-MODULES (undocumented) =back =head1 DESCRIPTION: This command runs "perl -c" on each module in a separate process and aggregates results. Running with --verbose will list specific modules instead of just a summary. Try "ur test use" for a faster evaluation of whether your software tree is broken. :) =cut ur-define-datasource-mysql.pod000444023532023421 220712121654175 20262 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur define datasource mysql - Add a MySQL data source to the current namespace. =head1 VERSION This document describes ur define datasource mysql version 0.29. =head1 SYNOPSIS ur define datasource mysql --auth=? --login=? [--nosingleton] --owner=? [--dsid=?] [--server=?] [DSNAME] =head1 REQUIRED ARGUMENTS =over =item auth I Password to log in with =item login I User to log in with =item nosingleton I Created data source should not inherit from UR::Singleton (defalt is that it will) Default value 'false' (--nonosingleton) if not specified =item nonosingleton I Make nosingleton 'false' =item owner I Owner/schema to connect to =back =head1 OPTIONAL ARGUMENTS =over =item dsid I The full class name to give this data source. =item server I "server" attribute for this data source, such as a database name =item DSNAME I The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'. =back =head1 DESCRIPTION: Add a MySQL data source to the current namespace. =cut ur-old-info.pod000444023532023421 62312121654175 15224 0ustar00abrummetgsc000000000000UR-0.41/pod =pod =head1 NAME ur old info - Outputs description(s) of UR entities such as classes and tables to stdout =head1 VERSION This document describes ur old info version 0.29. =head1 SYNOPSIS ur old info [SUBJECT] =head1 OPTIONAL ARGUMENTS =over =item SUBJECT (undocumented) =back =head1 DESCRIPTION: Outputs description(s) of UR entities such as classes and tables to stdout =cut lib000755023532023421 012121654175 12233 5ustar00abrummetgsc000000000000UR-0.41above.pm000444023532023421 522712121654173 14026 0ustar00abrummetgsc000000000000UR-0.41/libpackage above; use strict; use warnings; our $VERSION = '0.02'; sub import { my $package = shift; for (@_) { use_package($_); } } our %used_libs; BEGIN { %used_libs = ($ENV{PERL_USED_ABOVE} ? (map { $_ => 1 } split(":",$ENV{PERL_USED_ABOVE})) : ()); for my $path (keys %used_libs) { #print STDERR "Using (parent process') libraries at $path\n"; eval "use lib '$path';"; die "Failed to use library path '$path' from the environment PERL_USED_ABOVE?: $@" if $@; } }; sub use_package { my $class = shift; my $caller = (caller(1))[0]; my $module = $class; $module =~ s/::/\//g; $module .= ".pm"; ## paths already found in %used_above have ## higher priority than paths based on cwd for my $path (keys %used_libs) { if (-e "$path/$module") { eval "package $caller; use $class"; die $@ if $@; return; } } require Cwd; my $cwd = Cwd::cwd(); my @parts = ($cwd =~ /\//g); my $dirs_above = scalar(@parts); my $path=$cwd.'/'; until (-e "$path./$module") { if ($dirs_above == 0) { # Not found. Use the one out under test. # When deployed. $path = ""; last; }; #print "Didn't find it in $path, trying higher\n"; $path .= "../"; $dirs_above--; } # Get the special path in place if (length($path)) { while ($path =~ s:/[^/]+/\.\./:/:) { 1 } # simplify unless ($used_libs{$path}) { print STDERR "Using libraries at $path\n" unless $ENV{PERL_ABOVE_QUIET} or $ENV{COMP_LINE}; eval "use lib '$path';"; die $@ if $@; $used_libs{$path} = 1; my $env_value = join(":",sort keys %used_libs); $ENV{PERL_USED_ABOVE} = $env_value; } } # Now use the module. eval "package $caller; use $class"; die $@ if $@; }; 1; =pod =head1 NAME above - auto "use lib" when a module is in the tree of the PWD =head1 SYNOPSIS use above "My::Module"; =head1 DESCRIPTION Used by the command-line wrappers for Command modules which are developer tools. Do NOT use this in modules, or user applications. Uses a module as though the cwd and each of its parent directories were at the beginnig of @INC. If found in that path, the parent directory is kept as though by "use lib". =head1 EXAMPLES # given /home/me/perlsrc/My/Module.pm # in /home/me/perlsrc/My/Module/Some/Path/ # in myapp.pl: use above "My::Module"; # does this ..if run anywhere under /home/me/perlsrc: use lib '/home/me/perlsrc/' use My::Module; =head1 AUTHOR Scott Smith =cut Command.pm000444023532023421 32312121654175 14262 0ustar00abrummetgsc000000000000UR-0.41/libpackage Command; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is_abstract => 1, subclassify_by_version => 1, ); 1; UR.pm000444023532023421 10320312121654175 13313 0ustar00abrummetgsc000000000000UR-0.41/libpackage UR; # The UR module is itself a "UR::Namespace", besides being the root # module which bootstraps the system. The class definition itself # is made at the bottom of the file. use strict; use warnings FATAL => 'all'; # Set the version at compile time, since some other modules borrow it. our $VERSION = "0.41"; # UR $VERSION BEGIN { # unless otherwise specified, begin uncaching at 1 million objects #$ENV{'UR_CONTEXT_CACHE_SIZE_HIGHWATER'} ||= 1_000_000; #$ENV{'UR_CONTEXT_CACHE_SIZE_LOWWATER'} ||= 1_000; } # Ensure we get detailed errors while starting up. # This is disabled at the bottom of the module. use Carp; $SIG{__DIE__} = \&Carp::confess; # Ensure that, if the application changes directory, we do not # change where we load modules while running. use Cwd; my @PERL5LIB = ($ENV{PERL5LIB} ? split(':', $ENV{PERL5LIB}) : ()); for my $dir (@INC, @PERL5LIB) { next unless -d $dir; $dir = Cwd::abs_path($dir) || $dir; } $ENV{PERL5LIB} = join(':', @PERL5LIB); # Also need to fix modules that were already loaded, so that when # a namespace is loaded the path will not change out from # underneath it. for my $module (keys %INC) { $INC{$module} = Cwd::abs_path($INC{$module}); } # UR supports several environment variables, found under UR/ENV # Any UR_* variable which is set but does NOT corresponde to a module found will cause an exit # (a hedge against typos such as UR_DBI_NO_COMMMMIT=1 leading to unexpected behavior) for my $e (keys %ENV) { next unless substr($e,0,3) eq 'UR_'; eval "use UR::Env::$e"; if ($@) { my $path = __FILE__; $path =~ s/.pm$//; my @files = glob($path . '/Env/*'); my @vars = map { /UR\/Env\/(.*).pm/; $1 } @files; print STDERR "Environment variable $e set to $ENV{$e} but there were errors using UR::Env::$e:\n" . "Available variables:\n\t" . join("\n\t",@vars) . "\n"; exit 1; } } # These two dump info about used modules and libraries at program exit. END { if ($ENV{UR_USED_LIBS}) { print STDERR "Used library include paths (\@INC):\n"; for my $lib (@INC) { print STDERR "$lib\n"; } print STDERR "\n"; } if ($ENV{UR_USED_MODS}) { print STDERR "Used modules and paths (\%INC):\n"; for my $mod (sort keys %INC) { if ($ENV{UR_USED_MODS} > 1) { print STDERR "$mod => $INC{$mod}\n"; } else { print STDERR "$mod\n"; } } print STDERR "\n"; } if ($ENV{UR_DBI_SUMMARIZE_SQL}) { UR::DBI::print_sql_summary(); } } #Class::AutoloadCAN must be used before Class::Autouse, or the can methods will break in confusing ways. use Class::AutoloadCAN; use Class::Autouse; BEGIN { my $v = $Class::Autouse::VERSION; unless (($v =~ /^\d+\.?\d*$/ && $v >= 2.0) or $v eq '1.99_02' or $v eq '1.99_04') { die "UR requires Class::Autouse 2.0 or greater (or 1.99_02 or 1.99_04)!!"; } }; # Regular deps use Date::Format; # # Because UR modules execute code when compiling to define their classes, # and require each other for that code to execute, there are bootstrapping # problems. # # Everything which is part of the core framework "requires" UR # which, of course, executes AFTER it has compiled its SUBS, # but BEFORE it defines its class. # # Everything which _uses_ the core of the framework "uses" its namespace, # either the specific top-level namespace module, or "UR" itself for components/extensions. # require UR::Exit; require UR::Util; require UR::DBI::Report; # this is used by UR::DBI require UR::DBI; # this needs a new name, and need only be used by UR::DataSource::RDBMS require UR::ModuleBase; # this should be switched to a role require UR::ModuleConfig; # used by ::Time, and also ::Lock ::Daemon require UR::Object::Iterator; require UR::DeletedRef; require UR::Object; require UR::Object::Type; require UR::Object::Ghost; require UR::Object::Property; require UR::Observer; require UR::BoolExpr::Util; require UR::BoolExpr; # has meta require UR::BoolExpr::Template; # has meta require UR::BoolExpr::Template::PropertyComparison; # has meta require UR::BoolExpr::Template::Composite; # has meta require UR::BoolExpr::Template::And; # has meta require UR::BoolExpr::Template::Or; # has meta require UR::Object::Index; # # Define core metadata. # # This is done outside of the actual modules since the define() method # uses all of the modules themselves to do its work. # UR::Object::Type->define( class_name => 'UR::Object', is => [], # the default is to inherit from UR::Object, which is circular, so we explicitly say nothing is_abstract => 1, composite_id_separator => "\t", id_by => [ id => { is => 'Scalar', doc => 'unique identifier' } ] ); UR::Object::Type->define( class_name => "UR::Object::Index", id_by => ['indexed_class_name','indexed_property_string'], has => ['indexed_class_name','indexed_property_string'], is_transactional => 0, ); UR::Object::Type->define( class_name => 'UR::Object::Ghost', is_abstract => 1, ); UR::Object::Type->define( class_name => 'UR::Entity', extends => ['UR::Object'], is_abstract => 1, ); UR::Object::Type->define( class_name => 'UR::Entity::Ghost', extends => ['UR::Object::Ghost'], is_abstract => 1, ); # MORE METADATA CLASSES # For bootstrapping reasons, the properties with default values also need to be listed in # %class_property_defaults defined in UR::Object::Type::Initializer. If you make changes # to default values, please keep these in sync. UR::Object::Type->define( class_name => 'UR::Object::Type', doc => 'class/type meta-objects for UR', id_by => 'class_name', sub_classification_method_name => '_resolve_meta_class_name', is_abstract => 1, has => [ class_name => { is => 'Text', len => 256, is_optional => 1, doc => 'the name for the class described' }, properties => { is_many => 1, # this is calculated instead of a regular relationship # so we can do appropriate inheritance filtering. # We need an isa operator and its converse # in order to be fully declarative internally here calculate => 'shift->_properties(@_);', doc => 'property meta-objects for the class' }, id_properties => { is_many => 1, calculate => q( grep { defined $_->is_id } shift->_properties(@_) ), doc => 'meta-objects for the ID properties of the class' }, doc => { is => 'Text', len => 1024, is_optional => 1, doc => 'a one-line description of the class/type' }, is_abstract => { is => 'Boolean', default_value => 0, doc => 'abstract classes must be subclassified into a concreate class at create/load time' }, is_final => { is => 'Boolean', default_value => 0, doc => 'further subclassification is prohibited on final classes' }, is_transactional => { is => 'Boolean', default_value => 1, is_optional => 1, doc => 'non-transactional objects are left out of in-memory transactions' }, is_singleton => { is => 'Boolean', default_value => 0, doc => 'singleton classes have only one instance, or have each instance fall into a distinct subclass' }, namespace => { is => 'Text', len => 256, is_optional => 1, doc => 'the first "word" in the class name, which points to a UR::Namespace' }, schema_name => { is => 'Text', len => 256, is_optional => 1, doc => 'an arbitrary grouping for classes for which instances share a common storage system' }, data_source_id => { is => 'Text', len => 256, is_optional => 1, doc => 'for classes which persist beyond their current process, the identifier for their storage manager' }, #data_source_meta => { is => 'UR::DataSource', id_by => 'data_source_id', is_optional => 1, }, generated => { is => 'Boolean', is_transient => 1, default_value => 0, doc => 'an internal flag set when the class meta has fabricated accessors and methods in the class namespace' }, meta_class_name => { is => 'Text', doc => 'even meta-classess have a meta-class' }, composite_id_separator => { is => 'Text', len => 2 , default_value => "\t", is_optional => 1, doc => 'for classes whose objects have a multi-value "id", this overrides using a "\t" to compose/decompose' }, valid_signals => { is => 'ARRAY', is_optional => 1, doc => 'List of non-standard signal names observers can bind to ' }, # details used by the managment of the "real" entity outside of the app (persistence) table_name => { is => 'Text', len => undef, is_optional => 1, doc => 'for classes with a data source, this specifies the table or equivalent data structure which holds instances' }, select_hint => { is => 'Text', len => 1024 , is_optional => 1, doc => 'used to optimize access to underlying storage (database specific)' }, join_hint => { is => 'Text', len => 1024 , is_optional => 1, doc => 'used to optimize access to underlying storage when this class is part of a join (database specific)' }, id_generator => { is => 'Text', len => 256, is_optional => 1, doc => 'override the default choice for generating new object IDs' }, # different ways of handling subclassing at object load time subclassify_by => { is => 'Text', len => 256, is_optional => 1, doc => 'when set, the method specified will return the name of a specific subclass into which the object should go' }, subclass_description_preprocessor => { is => 'MethodName', len => 255, is_optional => 1, doc => 'a method which should pre-process the class description of sub-classes before construction' }, sub_classification_method_name => { is => 'Text', len => 256, is_optional => 1, doc => 'like subclassify_by, but examines whole objects not a single property' }, use_parallel_versions => { is => 'Boolean', is_optional => 1, default_value => 0, doc => 'inheriting from the is class will redirect to a ::V? module implemeting a specific version' }, # obsolete/internal type_name => { is => 'Text', len => 256, is_deprecated => 1, is_optional => 1 }, er_role => { is => 'Text', len => 256, is_optional => 1, default_value => 'entity' }, source => { is => 'Text', len => 256 , default_value => 'data dictionary', is_optional => 1 }, # This is obsolete and should be removed later sub_classification_meta_class_name => { is => 'Text', len => 1024 , is_optional => 1, doc => 'obsolete' }, first_sub_classification_method_name => { is => 'Text', len => 256, is_optional => 1, doc => 'cached value to handle a complex inheritance hierarchy with storage at some levels but not others' }, ### Relationships with the other meta-classes (used internally) ### # UR::Namespaces are singletons referenced through their name namespace_meta => { is => 'UR::Namespace', id_by => 'namespace' }, is => { is => 'ARRAY', is_mutable => 0, doc => 'List of the parent class names' }, # linking to the direct parents, and the complete ancestry parent_class_metas => { is => 'UR::Object::Type', id_by => 'is', doc => 'The list of UR::Object::Type objects for the classes that are direct parents of this class' },#, is_many => 1 }, parent_class_names => { via => 'parent_class_metas', to => 'class_name', is_many => 1 }, parent_meta_class_names => { via => 'parent_class_metas', to => 'meta_class_name', is_many => 1 }, ancestry_meta_class_names => { via => 'ancestry_class_metas', to => 'meta_class_name', is_many => 1 }, ancestry_class_metas => { is => 'UR::Object::Type', id_by => 'is', where => [-recurse => [class_name => 'is']], doc => 'Climb the ancestry tree and return the class objects for all of them' }, ancestry_class_names => { via => 'ancestry_class_metas', to => 'class_name', is_many => 1 }, # This one isn't useful on its own, but is used to build the all_* accessors below all_class_metas => { is => 'UR::Object::Type', calculate => 'return ($self, $self->ancestry_class_metas)' }, # Properties defined on this class, parent classes, etc. # There's also a property_meta_by_name() method defined in the class direct_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', is_many => 1 }, direct_property_names => { via => 'direct_property_metas', to => 'property_name', is_many => 1 }, direct_id_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', where => [ 'is_id true' => 1, -order_by => 'is_id' ], is_many => 1 }, direct_id_property_names => { via => 'direct_id_property_metas', to => 'property_name', is_many => 1 }, ancestry_property_metas => { via => 'ancestry_class_metas', to => 'direct_property_metas', is_many => 1 }, ancestry_property_names => { via => 'ancestry_class_metas', to => 'direct_property_names', is_many => 1 }, ancestry_id_property_metas => { via => 'ancestry_class_metas', to => 'direct_id_property_metas', is_many => 1 }, ancestry_id_property_names => { via => 'ancestry_id_property_metas', to => 'property_name', is_many => 1 }, all_property_metas => { via => 'all_class_metas', to => 'direct_property_metas', is_many => 1 }, all_property_names => { via => 'all_property_metas', to => 'property_name', is_many => 1 }, all_id_property_metas => { via => 'all_class_metas', to => 'direct_id_property_metas', is_many => 1 }, all_id_property_names => { via => 'all_id_property_metas', to => 'property_name', is_many => 1 }, direct_id_by_property_metas => { via => 'direct_property_metas', to => '__self__', where => ['id_by true' => 1], is_many => 1, doc => "Properties with 'id_by' metadata, ie. direct object accessor properties" } , all_id_by_property_metas => { via => 'all_class_metas', to => 'direct_id_by_property_metas', is_many => 1}, direct_reverse_as_property_metas => { via => 'direct_property_metas', to => '__self__', where => ['reverse_as true' => 1], is_many => 1, doc => "Properties with 'reverse_as' metadata, ie. indirect object accessor properties" }, all_reverse_as_property_metas => { via => 'all_class_metas', to => 'direct_reverse_as_property_metas', is_many => 1}, # Datasource related stuff direct_column_names => { via => 'direct_property_metas', to => 'column_name', is_many => 1, where => [column_name => { operator => 'true' }] }, direct_id_column_names => { via => 'direct_id_property_metas', to => 'column_name', is_many => 1, where => [column_name => { operator => 'true'}] }, ancestry_column_names => { via => 'ancestry_class_metas', to => 'direct_column_names', is_many => 1 }, ancestry_id_column_names => { via => 'ancestry_class_metas', to => 'direct_id_column_names', is_many => 1 }, # Are these *columnless* properties actually necessary? The user could just use direct_property_metas(column_name => undef) direct_columnless_property_metas => { is => 'UR::Object::Property', reverse_as => 'class_meta', where => [column_name => undef], is_many => 1 }, direct_columnless_property_names => { via => 'direct_columnless_property_metas', to => 'property_name', is_many => 1 }, ancestry_columnless_property_metas => { via => 'ancestry_class_metas', to => 'direct_columnless_property_metas', is_many => 1 }, ancestry_columnless_property_names => { via => 'ancestry_columnless_property_metas', to => 'property_name', is_many => 1 }, ancestry_table_names => { via => 'ancestry_class_metas', to => 'table_name', is_many => 1 }, all_table_names => { via => 'all_class_metas', to => 'table_name', is_many => 1 }, all_column_names => { via => 'all_class_metas', to => 'direct_column_names', is_many => 1 }, all_id_column_names => { via => 'all_class_metas', to => 'direct_id_column_names', is_many => 1 }, all_columnless_property_metas => { via => 'all_class_metas', to => 'direct_columnless_property_metas', is_many => 1 }, all_columnless_property_names => { via => 'all_class_metas', to => 'direct_columnless_property_names', is_many => 1 }, ], ); UR::Object::Type->define( class_name => 'UR::Object::Property', id_properties => [ class_name => { is => 'Text', len => 256 }, property_name => { is => 'Text', len => 256 }, ], has_optional => [ property_type => { is => 'Text', len => 256 , is_optional => 1}, column_name => { is => 'Text', len => 256, is_optional => 1 }, data_length => { is => 'Text', len => 32, is_optional => 1 }, data_type => { is => 'Text', len => 256, is_optional => 1 }, default_value => { is_optional => 1 }, valid_values => { is => 'ARRAY', is_optional => 1, }, example_values => { is => 'ARRAY', is_optional => 1, }, doc => { is => 'Text', len => 1000, is_optional => 1 }, is_id => { is => 'Integer', default_value => undef, doc => 'denotes this is an ID property of the class, and ranks them' }, is_optional => { is => 'Boolean' , default_value => 0}, is_transient => { is => 'Boolean' , default_value => 0}, is_constant => { is => 'Boolean' , default_value => 0}, # never changes is_mutable => { is => 'Boolean' , default_value => 1}, # can be changed explicitly via accessor (cannot be constant) is_volatile => { is => 'Boolean' , default_value => 0}, # changes w/o a signal: (cannot be constant or transactional) is_classwide => { is => 'Boolean' , default_value => 0}, is_delegated => { is => 'Boolean' , default_value => 0}, is_calculated => { is => 'Boolean' , default_value => 0}, is_transactional => { is => 'Boolean' , default_value => 1}, # STM works on these, and the object can possibly save outside the app is_abstract => { is => 'Boolean' , default_value => 0}, is_concrete => { is => 'Boolean' , default_value => 1}, is_final => { is => 'Boolean' , default_value => 0}, is_many => { is => 'Boolean' , default_value => 0}, is_aggregate => { is => 'Boolean' , default_value => 0}, is_deprecated => { is => 'Boolean', default_value => 0}, is_numeric => { calculate_from => ['data_type'], }, id_by => { is => 'ARRAY', is_optional => 1}, id_class_by => { is => 'Text', is_optional => 1}, is_undocumented => { is => 'Boolean', is_optional => 1, doc => 'do not show in documentation to users' }, doc_position => { is => 'Number', is_optional => 1, doc => 'override the sort position within documentation' }, access_as => { is => 'Text', is_optional => 1, doc => 'when id_class_by is set, and this is set to "auto", primitives will return as their ID instead of boxed' }, order_by => { is => 'ARRAY', is_optional => 1}, specify_by => { is => 'Text', is_optional => 1}, reverse_as => { is => 'ARRAY', is_optional => 1 }, implied_by => { is => 'Text' , is_optional => 1}, via => { is => 'Text' , is_optional => 1 }, to => { is => 'Text' , is_optional => 1}, where => { is => 'ARRAY', is_optional => 1}, calculate => { is => 'Text' , is_optional => 1}, calculate_from => { is => 'ARRAY' , is_optional => 1}, calculate_perl => { is => 'Perl' , is_optional => 1}, calculate_sql => { is => 'SQL' , is_optional => 1}, calculate_js => { is => 'JavaScript' , is_optional => 1}, constraint_name => { is => 'Text' , is_optional => 1}, is_legacy_eav => { is => 'Boolean' , is_optional => 1}, is_dimension => { is => 'Boolean', is_optional => 1}, is_specified_in_module_header => { is => 'Boolean', default_value => 0 }, position_in_module_header => { is => 'Integer', is_optional => 1, doc => "Line in the class definition source's section this property appears" }, singular_name => { is => 'Text' }, plural_name => { is => 'Text' }, class_meta => { is => 'UR::Object::Type', id_by => 'class_name' }, r_class_meta => { is => 'UR::Object::Type', id_by => 'data_type' }, ], unique_constraints => [ { properties => [qw/property_name class_name/], sql => 'SUPER_FAKE_O4' }, ], ); UR::Object::Type->define( class_name => 'UR::Object::Property::Calculated::From', id_properties => [qw/class_name calculated_property_name source_property_name/], ); require UR::Singleton; require UR::Namespace; UR::Object::Type->define( class_name => 'UR', extends => ['UR::Namespace'], ); require UR::Context; UR::Object::Type->initialize_bootstrap_classes; require Command; $UR::initialized = 1; require UR::Change; require UR::Context::Root; require UR::Context::Process; require UR::Object::Tag; do { UR::Context->_initialize_for_current_process(); }; require UR::ModuleLoader; # signs us up with Class::Autouse require UR::Value::Iterator; require UR::Object::View; require UR::Object::Join; sub main::ur_core { print STDERR "Dumping rules and templates to ./ur_core.stor...\n"; my $dump; unless(open($dump, ">ur_core.stor")) { print STDERR "Can't open ur_core.stor for writing: $!"; exit; } store_fd([ $UR::Object::rule_templates, $UR::Object::rules, ], $dump); close $dump; exit(); } 1; __END__ =pod =head1 NAME UR - rich declarative transactional objects =head1 VERSION This document describes UR version 0.41 =head1 SYNOPSIS use UR; ## no database class Foo { is => 'Bar', has => [qw/prop1 prop2 prop3/] }; $o1 = Foo->create(prop1 => 111, prop2 => 222, prop3 => 333); @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object $o1->delete; @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns zero objects @o = Foo->get(prop2 => 222, prop1 => [101,111,121], 'prop3 between' => [200, 400]); # returns one object again ## database class Animal { has => [ favorite_food => { is => 'Text', doc => "what's yummy?" }, ], data_source => 'MyDB1', table_name => 'Animal' }; class Cat { is => 'Animal', has => [ feet => { is => 'Number', default_value => 4 }, fur => { is => 'Text', valid_values => [qw/fluffy scruffy/] }, ], data_source => 'MyDB1', table_name => 'Cat' }; Cat->create(feet => 4, fur => 'fluffy', favorite_food => 'taters'); @cats = Cat->get(favorite_food => ['taters','sea bass']); $c = $cats[0]; print $c->feet,"\n"; $c->fur('scruffy'); UR::Context->commit(); =head1 DESCRIPTION UR is a class framework and object/relational mapper for Perl. It starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and extends its capabilities with ORM (object-relational mapping) capabilities, object cache, in-memory transactions, more formal class definitions, metadata, documentation system, iterators, command line tools, etc. UR can handle multiple column primary and foreign keys, SQL joins involving class inheritance and relationships, and does its best to avoid querying the database unless the requested data has not been loaded before. It has support for SQLite, Oracle, Mysql and Postgres databases, and the ability to use a text file as a table. UR uses the same syntax to define non-persistent objects, and supports in-memory transactions for both. =head1 DOCUMENTATION =head2 Manuals L - command line interface L - UR from Ten Thousand Feet L - Getting started with UR L - Slides for a presentation on UR L - Recepies for getting stuff working L - UR's metadata system L - Defining classes =head2 Basic Entities L - Pretty much everything is-a UR::Object L - Metadata class for Classes L - Metadata class for Properties L - Manage packages and classes L - Software transactions and More! L - How and where to get data =head1 QUICK TUTORIAL First create a Namespace class for your application, Music.pm: package Music; use UR; class Music { is => 'UR::Namespace' }; 1; Next, define a data source representing your database, Music/DataSource/DB1.pm package Music::DataSource::DB1; use Music; class Music::DataSource::DB1 { is => ['UR::DataSource::MySQL', 'UR::Singleton'], has_constant => [ server => { value => 'database=music' }, owner => { value => 'music' }, login => { value => 'mysqluser' }, auth => { value => 'mysqlpasswd' }, ] }; or to get something going quickly, SQLite has smart defaults... class Music::DataSource::DB1 { is => ['UR::DataSource::SQLite', 'UR::Singleton'], }; Create a class to represent artists, who have many CDs, in Music/Artist.pm package Music::Artist; use Music; class Music::Artist { id_by => 'artist_id', has => [ name => { is => 'Text' }, cds => { is => 'Music::Cd', is_many => 1, reverse_as => 'artist' } ], data_source => 'Music::DataSource::DB1', table_name => 'ARTIST', }; Create a class to represent CDs, in Music/Cd.pm package Music::Cd; use Music; class Music::Cd { id_by => 'cd_id', has => [ artist => { is => 'Music::Artist', id_by => 'artist_id' }, title => { is => 'Text' }, year => { is => 'Integer' }, artist_name => { via => 'artist', to => 'name' }, ], data_source => 'Music::DataSource::DB1', table_name => 'CD', }; If the database does not exist, you can run this to generate the tables and columns from the classes you've written (very experimental): $ cd Music $ ur update schema If the database existed already, you could have done this to get it to write the last 2 classes for you: $ cd Music; $ ur update classes Regardless, if the classes and database tables are present, you can then use these classes in your application code: # Using the namespace enables auto-loading of modules upon first attempt to call a method use Music; # This would get back all Artist objects: my @all_artists = Music::Artist->get(); # After the above, further requests would be cached # if that set were large though, you might want to iterate gradually: my $artist_iter = Music::Artist->create_iterator(); # Get the first object off of the iterator my $first_artist = $artist_iter->next(); # Get all the CDs published in 2007 for the first artist my @cds_2007 = Music::Cd->get(year => 2007, artist => $first_artist); # Use non-equality operators: my @some_cds = Music::Cd->get( 'year between' => ['2004','2009'] ); # This will use a JOIN with the ARTISTS table internally to filter # the data in the database. @some_cds will contain Music::Cd objects. # As a side effect, related Artist objects will be loaded into the cache @some_cds = Music::Cd->get( year => '2007', 'artist_name like' => 'Bob%' ); # These values would be cached... my @artists_for_some_cds = map { $_->artist } @some_cds; # This will use a join to prefetch Artist objects related to the # objects that match the filter my @other_cds = Music::Cd->get( 'title like' => '%White%', -hints => ['artist'] ); my $other_artist_0 = $other_cds[0]->artist; # already loaded so no query # create() instantiates a new object in the current "context", but does not save # it in the database. It will autogenerate its own cd_id: my $new_cd = Music::Cd->create( title => 'Cool Album', year => 2009 ); # Assign it to an artist; fills in the artist_id field of $new_cd $first_artist->add_cd($new_cd); # Save all changes in the current transaction back to the database(s) # which are behind the changed objects. UR::Context->current->commit; =head1 Environment Variables UR uses several environment variables to do things like run with database commits disabled, watching SQL queries run, examine query plans, and control cache size, etc. These make development and debugging fast and easy. See L for details. =head1 DEPENDENCIES Class::Autouse Cwd Data::Dumper Date::Format DBI File::Basename FindBin FreezeThaw Path::Class Scalar::Util Sub::Installer Sub::Name Sys::Hostname Text::Diff Time::HiRes XML::Simple =head1 AUTHORS UR was built by the software development team at The Genome Institute at Washington University School of Medicine (Richard K. Wilson, PI). Incarnations of it run laboratory automation and analysis systems for high-throughput genomics. Anthony Brummett brummett@cpan.org Nathan Nutter Josh McMichael Eric Clark Ben Oberkfell Eddie Belter Feiyu Du Adam Dukes Brian Derickson Craig Pohl Gabe Sanderson Todd Hepler Jason Walker James Weible Indraniel Das Shin Leong Ken Swanson Scott Abbott Alice Diec William Schroeder Shawn Leonard Lynn Carmichael Amy Hawkins Michael Kiwala Kevin Crouse Mark Johnson Kyung Kim Jon Schindler Justin Lolofie Jerome Peirick Ryan Richt John Osborne Chris Harris Philip Kimmey Robert Long Travis Abbott Matthew Callaway James Eldred Scott Smith sakoht@cpan.org David Dooling =head1 LICENCE AND COPYRIGHT Copyright (C) 2002-2011 Washington University in St. Louis, MO. This sofware is licensed under the same terms as Perl itself. See the LICENSE file in this distribution. =pod Command000755023532023421 012121654175 13611 5ustar00abrummetgsc000000000000UR-0.41/libV2.pm000444023532023421 3072512121654172 14617 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::V2; use strict; use warnings; use UR; use Data::Dumper; use File::Basename; use Getopt::Long; use Command::View::DocMethods; use Command::Dispatch::Shell; our $VERSION = "0.41"; # UR $VERSION; our $entry_point_class; our $entry_point_bin; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', is_abstract => 1, subclass_description_preprocessor => 'Command::V2::_preprocess_subclass_description', attributes_have => [ is_param => { is => 'Boolean', is_optional => 1 }, is_input => { is => 'Boolean', is_optional => 1 }, is_output => { is => 'Boolean', is_optional => 1 }, shell_args_position => { is => 'Integer', is_optional => 1, doc => 'when set, this property is a positional argument when run from a shell' }, completion_handler => { is => 'MethodName', is_optional => 1, doc => 'to supply auto-completions for this parameter, call this class method' }, require_user_verify => { is => 'Boolean', is_optional => 1, doc => 'when expanding user supplied values: 0 = never verify, 1 = always verify, undef = determine automatically', }, ], has_optional => [ is_executed => { is => 'Boolean' }, result => { is => 'Scalar', is_output => 1 }, original_command_line => { is => 'String', doc => 'null-byte separated list of command and arguments when run via execute_with_shell_params_and_exit'}, _total_command_count => { is => 'Integer', default => 0, is_transient => 1 }, _command_errors => { is => 'HASH', doc => 'Values can be an array ref is multiple errors occur during a command\'s execution', default => {}, is_transient => 1, }, ], ); sub _is_hidden_in_docs { return; } sub _preprocess_subclass_description { my ($class, $desc) = @_; while (my ($prop_name, $prop_desc) = each(%{ $desc->{has} })) { unless ( $prop_desc->{'is_param'} or $prop_desc->{'is_input'} or $prop_desc->{'is_transient'} or $prop_desc->{'is_calculated'}, or $prop_desc->{'is_output'} ) { $prop_desc->{'is_param'} = 1; } } return $desc; } sub _init_subclass { # Each Command subclass has an automatic wrapper around execute(). # This ensures it can be called as a class or instance method, # and that proper handling occurs around it. my $subclass_name = $_[0]; no strict; no warnings; if ($subclass_name->can('execute')) { # NOTE: manipulating %{ $subclass_name . '::' } directly causes ptkdb to segfault perl my $new_symbol = "${subclass_name}::_execute_body"; my $old_symbol = "${subclass_name}::execute"; *$new_symbol = *$old_symbol; undef *$old_symbol; } else { #print "no execute in $subclass_name\n"; } if($subclass_name->can('shortcut')) { my $new_symbol = "${subclass_name}::_shortcut_body"; my $old_symbol = "${subclass_name}::shortcut"; *$new_symbol = *$old_symbol; undef *$old_symbol; } my @p = $subclass_name->__meta__->properties(); my @e; for my $p (@p) { next if $p->property_name eq 'id'; next if $p->class_name eq __PACKAGE__; next unless $p->class_name->isa('Command'); unless ($p->is_input or $p->is_output or $p->is_param or $p->is_transient or $p->is_calculated) { my $modname = $subclass_name; $modname =~ s|::|/|g; $modname .= '.pm'; push @e, $modname . " property " . $p->property_name . " must be input, output, param, transient, or calculated!"; } } if (@e) { for (@e) { $subclass_name->error_message($_); } die "command classes like $subclass_name have properties without is_input/output/param/transient/calculated set!"; } return 1; } sub create { my $class = shift; my ($rule,%extra) = $class->define_boolexpr(@_); my @params_list = $rule->params_list; my $self = $class->SUPER::create(@params_list, %extra); return unless $self; # set non-optional boolean flags to false. # TODO: rename that property meta method if it is not ONLY used for shell args for my $property_meta ($self->_shell_args_property_meta) { my $property_name = $property_meta->property_name; if (!$property_meta->is_optional and !defined($self->$property_name)) { if (defined $property_meta->data_type and $property_meta->data_type =~ /Boolean/i) { $self->$property_name(0); } } } return $self; } sub __errors__ { my ($self,@property_names) = @_; my @errors1 =($self->SUPER::__errors__); if ($self->is_executed) { return @errors1; } # for Commands which have not yet been executed, # only consider errors on inputs or params my $meta = $self->__meta__; my @errors2; ERROR: for my $e (@errors1) { for my $p ($e->properties) { my $pm = $meta->property($p); if ($pm->is_input or $pm->is_param) { push @errors2, $e; next ERROR; } } } return @errors2; } # For compatability with Command::V1 callers sub is_sub_command_delegator { return; } sub shortcut { my $self = shift; return unless $self->can('_shortcut_body'); my $result = $self->_shortcut_body; $self->result($result); return $result; } sub execute { # This is a wrapper for real execute() calls. # All execute() methods are turned into _execute_body at class init, # so this will get direct control when execute() is called. my $self = shift; #TODO handle calls to SUPER::execute() from another execute(). # handle calls as a class method my $was_called_as_class_method = 0; if (ref($self)) { if ($self->is_executed) { Carp::confess("Attempt to re-execute an already executed command."); } } else { # called as class method # auto-create an instance and execute it $self = $self->create(@_); return unless $self; $was_called_as_class_method = 1; } # handle __errors__ objects before execute if (my @problems = $self->__errors__) { for my $problem (@problems) { my @properties = $problem->properties; $self->error_message("Property " . join(',', map { "'$_'" } @properties) . ': ' . $problem->desc); } $self->delete() if $was_called_as_class_method; return; } my $result = $self->_execute_body(@_); $self->is_executed(1); $self->result($result); return $self if $was_called_as_class_method; return $result; } sub _execute_body { # default implementation in the base class # Override "execute" or "_execute_body" to implement the body of the command. # See above for details of internal implementation. my $self = shift; my $class = ref($self) || $self; if ($class eq __PACKAGE__) { die "The execute() method is not defined for $_[0]!"; } return 1; } sub exit_code_for_return_value { my $self = shift; my $return_value = shift; # Translates a true/false value from the command module's execute() # from Perl (where positive means success), to shell (where 0 means success) # Also, execute() could return a negative value; this is converted to # positive and used as the shell exit code. NOTE: This means execute() # returning 0 and -1 mean the same thing if (! $return_value) { $return_value = 1; } elsif ($return_value < 0) { $return_value = 0 - $return_value; } else { $return_value = 0 } return $return_value; } sub display_command_summary_report { my $self = shift; my $total_count = $self->_total_command_count; my %command_errors = %{$self->_command_errors}; if (keys %command_errors) { $self->status_message("\n\nErrors Summary:"); for my $key (keys %command_errors) { my $errors = $command_errors{$key}; $errors = [$errors] unless (ref($errors) and ref($errors) eq 'ARRAY'); my @errors = @{$errors}; print "$key: \n"; for my $error (@errors) { $error = $self->truncate_error_message($error); print "\t- $error\n"; } } } if ($total_count > 1) { my $error_count = scalar(keys %command_errors); $self->status_message("\n\nCommand Summary:"); $self->status_message(" Successful: " . ($total_count - $error_count)); $self->status_message(" Errors: " . $error_count); $self->status_message(" Total: " . $total_count); } } sub append_error { my $self = shift; my $key = shift || die; my $error = shift || die; my $command_errors = $self->_command_errors; push @{$command_errors->{$key}}, $error; $self->_command_errors($command_errors); return 1; } sub truncate_error_message { my $self = shift; my $error = shift || die; # truncate errors so they are actually a summary ($error) = split("\n", $error); # meant to truncate a callstack as this is meant for user/high-level $error =~ s/\ at\ \/.*//; return $error; } 1; __END__ =pod =head1 NAME Command - base class for modules implementing the command pattern =head1 SYNOPSIS use TopLevelNamespace; class TopLevelNamespace::SomeObj::Command { is => 'Command', has => [ someobj => { is => 'TopLevelNamespace::SomeObj', id_by => 'some_obj_id' }, verbose => { is => 'Boolean', is_optional => 1 }, ], }; sub execute { my $self = shift; if ($self->verbose) { print "Working on id ",$self->some_obj_id,"\n"; } my $result = $someobj->do_something(); if ($self->verbose) { print "Result was $result\n"; } return $result; } sub help_brief { return 'Call do_something on a SomeObj instance'; } sub help_synopsis { return 'cmd --some_obj_id 123 --verbose'; } sub help_detail { return 'This command performs a FooBarBaz transform on a SomObj object instance by calling its do_something method.'; } # Another part of the code my $cmd = TopLevelNamespace::SomeObj::Command->create(some_obj_id => $some_obj->id); $cmd->execute(); =head1 DESCRIPTION The Command module is a base class for creating other command modules implementing the Command Pattern. These modules can be easily reused in applications or loaded and executed dynamicaly in a command-line program. Each Command subclass represents a reusable work unit. The bulk of the module's code will likely be in the execute() method. execute() will usually take only a single argument, an instance of the Command subclass. =head1 Command-line use Creating a top-level Command module called, say TopLevelNamespace::Command, and a script called tln_cmd that looks like: #!/usr/bin/perl use TopLevelNamespace; TopLevelNamespace::Command->execute_with_shell_params_and_exit(); gives you an instant command-line tool as an interface to the hierarchy of command modules at TopLevelNamespace::Command. For example: > tln_cmd foo bar --baz 1 --qux will create an instance of TopLevelNamespace::Command::Foo::Bar (if that class exists) with params baz => 1 and qux => 1, assumming qux is a boolean property, call execute() on it, and translate the return value from execute() into the appropriate notion of a shell return value, meaning that if execute() returns true in the Perl sense, then the script returns 0 - true in the shell sense. The infrastructure takes care of turning the command line parameters into parameters for create(). Params designated as is_optional are, of course, optional and non-optional parameters that are missing will generate an error. --help is an implicit param applicable to all Command modules. It generates some hopefully useful text based on the documentation in the class definition (the 'doc' attributes you can attach to a class and properties), and the strings returned by help_detail(), help_brief() and help_synopsis(). =head1 TODO This documentation needs to be fleshed out more. There's a lot of special things you can do with Command modules that isn't mentioned here yet. =cut DynamicSubCommands.pm000444023532023421 1741012121654173 20045 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::DynamicSubCommands; use strict; use warnings; use UR; class Command::DynamicSubCommands { is => 'Command', is_abstract => 1, }; sub _init_subclass { my $subclass = shift; my $meta = $subclass->__meta__; if (grep { $_ eq __PACKAGE__ } $meta->parent_class_names) { my $delegating_class_name = $subclass; eval "sub ${subclass}::_delegating_class_name { '$delegating_class_name' }"; } return 1; } sub __extend_namespace__ { # auto generate sub-classes at the time of first reference my ($self,$ext) = @_; my $meta = $self->SUPER::__extend_namespace__($ext); return $meta if $meta; unless ($self->can('_sub_commands_from')) { die "Class " . $self->class . " does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $self->_sub_commands_from; my $target_class_name = join('::', $ref_class, $ext); my $target_class_meta = UR::Object::Type->get($target_class_name); if ($target_class_meta and $target_class_name->isa($ref_class)) { my $subclass_name = join('::', $self->class, $ext); my $subclass = $self->_build_sub_command($subclass_name, $self->class, $target_class_name); my $meta = $subclass->__meta__; return $meta; } return; } sub _build_all_sub_commands { my ($class) = @_; unless ($class->can('_sub_commands_from')) { die "Class $class does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $class->_sub_commands_from; my $delegating_class_name = $class; my $module = $ref_class; $module =~ s/::/\//g; $module .= '.pm'; my $base_path = $INC{$module}; unless ($base_path) { if (UR::Object::Type->get($ref_class)) { $base_path = $INC{$module}; } unless ($base_path) { die "Failed to find the path for ref class $ref_class!"; } } $base_path =~ s/$module//; my $ref_path = $ref_class; $ref_path =~ s/::/\//g; my $full_ref_path = $base_path . '/' . $ref_path; my @target_paths = glob("$full_ref_path/*.pm"); my @target_class_names; for my $target_path (@target_paths) { my $target = $target_path; $target =~ s#$base_path\/$ref_path/##; $target =~ s/\.pm//; my $target_class_name = $ref_class . '::' . $target; my $target_meta = UR::Object::Type->get($target_class_name); next unless $target_meta; next unless $target_class_name->isa($ref_class); push @target_class_names, $target => $target_class_name; } my %target_classes = @target_class_names; my @subclasses; for my $target (sort keys %target_classes) { my $target_class_name = $target_classes{$target}; my $class_name = $delegating_class_name . '::' . $target; # skip commands which have a module my $module_name = $class_name; $module_name =~ s|::|/|g; $module_name .= '.pm'; if (my @matches = grep { -e $_ . '/' . $module_name } @INC) { my $c = UR::Object::Type->get($class_name); push @subclasses, $class_name; next; } my @new_class_names = $class->_build_sub_command($class_name,$delegating_class_name,$target_class_name); for my $new_class_name (@new_class_names) { eval "sub ${new_class_name}::_target_class_name { '$target_class_name' }"; push @subclasses, $new_class_name; } } return @subclasses; } sub _build_sub_command { my ($self,$class_name,$delegating_class_name,$reference_class_name) = @_; class {$class_name} { is => $delegating_class_name, doc => '', }; return $class_name; } sub sub_command_dirs { my $class = ref($_[0]) || $_[0]; return ( $class eq $class->_delegating_class_name ? 1 : 0 ); } sub sub_command_classes { my $class = shift; unless(exists $class->__meta__->{_sub_commands}) { my @subclasses = $class->_build_all_sub_commands; $class->__meta__->{_sub_commands} = \@subclasses; } return @{ $class->__meta__->{_sub_commands} }; } sub _target_class_name { undef } 1; =pod =head1 NAME Command::DynamicSubCommands - auto-generate sub-commands based on other classes =head1 SYNOPSIS # given that these classes exist: # Acme::Task::Foo # Acme::Task::Bar # in Acme/Worker/Command/DoTask.pm: class Acme::Worker::Command::DoTask { is => 'Command::DynamicSubCommands', has => [ param1 => { is => 'Text' }, param2 => { is => 'Text' }, ] }; sub _sub_commands_from { 'Acme::Task' } sub execute { my $self = shift; print "this command " . ref($self) . " applies to " . $self->_target_class_name; return 1; } # the class above will discover them at compile, # and auto-generate these subclasses of itself: # Acme::Worker::Command::DoTask::Foo # Acme::Worker::Command::DoTask::Bar # in the shell... # # $ acme worker do-task # foo # bar # # $ acme worker do-task foo --param1 aaa --param2 bbb # this command Acme::Worker::Command::DoTask::Foo applies to Acme::Task::Foo # # $ acme worker do-task bar --param1 ccc --param2 ddd # this command Acme::Worker::Command::DoTask::Bar applies to Acme::Task::Bar =head1 DESCRIPTION This module helps you avoid writing boilerplate commands. When a command has a set of sub-commands which are meant to be derived from another group of classes, this module lets you auto-generate those sub-commands at run time. =head1 REQUIRED ABSTRACT METHOD =over 4 =item _sub_commands_from $base_namespace = Acme::Order::Command->_sub_commands_from(); # 'Acme::Task Returns the namespace from which target classes will be discovered, and sub-commands will be generated. =back =head1 PRIVATE API =over 4 =item _target_class_name $c= Acme::Order::Command::Purchasing->_target_class_name; # 'Acme::Task::Foo' The name of some class under the _sub_commands_from() namespace. This value is set during execute, revealing which sub-command the caller is using. =back =head1 OPTIONAL OVERRIDES =over 4 =item _build_sub_commmand This can be overridden to customize the sub-command construction. By default, each target under _sub_commands_from will result in a call to this method. The default implementation is below: my $self = shift; my ($suggested_class_name,$delegator_class_name,$target_class_name) = @_; class {$suggested_class_name} { is => $delegator_class_name, sub_classify_by => 'class', has_constant => [ _target_class_name => { value => $target_class_name }, ] }; return ($suggested_class_name); Note that the class in question may be on the filesystem, and not need to be created. The return list can include more than one class name, or zero class names. =item _build_all_sub_commands This is called once for any class which inherits from Command::DynamicSubCommands. It generates the sub-commands as needed, and returns a list. By default it resolves the target classes, and calls _build_sub_command It can be overridden to customize behavior, or filter results. Be sure to call @cmds = $self->SUPER::_build_all_sub_commands() if you want to get the default commands in addition to overriding. =back The sub-commands need not be 1:1 with the target classes, though this is the default. The sub-commands need not inherit from the Command::DynamicSubCommands base command which generates them, though this is the default. =cut Tree.pm000444023532023421 4024512121654173 15226 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::Tree; use strict; use warnings; use UR; use File::Basename qw/basename/; our $VERSION = "0.41"; # UR $VERSION; class Command::Tree { is => 'Command::V2', is_abstract => 1, doc => 'base class for commands which delegate to sub-commands', }; sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; if ( $argv[0] and $argv[0] !~ /^\-/ and my $class_for_sub_command = $self->class_for_sub_command($argv[0]) ) { # delegate shift @argv; return $class_for_sub_command->resolve_class_and_params_for_argv(@argv); } elsif ( @argv == 1 and $argv[0] =~ /^(\-)?\-h(elp)?$/ ) { # HELP ME! return ($self, { help => 1 }); } else { # error return ($self,undef); } } sub resolve_option_completion_spec { my $class = shift; my @completion_spec; my @sub = eval { $class->sub_command_names }; if ($@) { $class->warning_message("Couldn't load class $class: $@\nSkipping $class..."); return; } for my $sub (@sub) { my $sub_class = $class->class_for_sub_command($sub); my $sub_tree = $sub_class->resolve_option_completion_spec() if defined($sub_class); # Hack to fix several broken commands, this should be removed once commands are fixed. # If the commands were not broken then $sub_tree will always exist. # Basically if $sub_tree is undef then we need to remove '>' to not break the OPTS_SPEC if ($sub_tree) { push @completion_spec, '>' . $sub => $sub_tree; } else { if (defined $sub_class) { print "WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; } else { print "WARNING: $sub has no sub_class so could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; } push @completion_spec, $sub => undef; } } push @completion_spec, "help!" => undef; return \@completion_spec } sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } return ""; } } sub doc_help { my $self = shift; my $command_name = $self->command_name; my $text; # show the list of sub-commands $text = sprintf( "Sub-commands for %s:\n%s", Term::ANSIColor::colored($command_name, 'bold'), $self->help_sub_commands, ); return $text; } sub doc_manual { my $self = shift; my $pod = $self->_doc_name_version; my $manual = $self->_doc_manual_body; my $help = $self->help_detail; if ($manual or $help) { $pod .= "=head1 DESCRIPTION:\n\n"; my $txt = $manual || $help; if ($txt =~ /^\=/) { # pure POD $pod .= $manual; } else { $txt =~ s/\n/\n\n/g; $pod .= $txt; #$pod .= join('', map { " $_\n" } split ("\n",$txt)) . "\n"; } } my $sub_commands = $self->help_sub_commands(brief => 1); $pod .= "=head1 SUB-COMMANDS\n\n" . $sub_commands . "\n\n"; $pod .= $self->_doc_footer(); $pod .= "\n\n=cut\n\n"; return "\n$pod"; } sub sorted_sub_command_classes { no warnings; my @c = map { [ $_->sub_command_sort_position, $_ ] } shift->sub_command_classes; return map { $_->[1] } sort { ($a->[0] <=> $b->[0]) || ($a->[0] cmp $b->[0]) } @c; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub _categorize_sub_commands { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @order; for my $sub_command_class (@sub_command_classes) { next if $sub_command_class->_is_hidden_in_docs(); my $category = $sub_command_class->sub_command_category || ''; unless (exists $categories{$category}) { if ($category) { push(@order, $category) } else { unshift(@order, ''); } $categories{$category} = []; } push(@{$categories{$category}}, $sub_command_class); } return (\@order, \%categories); } sub help_sub_commands { my ($self, %params) = @_; my ($order, $categories) = $self->_categorize_sub_commands(@_); my $command_name_method = 'command_name_brief'; no warnings; local $Text::Wrap::columns = 60; my @full_data; for my $category (@$order) { my $sub_commands_within_this_category = $categories->{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, ($_->isa('Command::Tree') ? '...' : ''), #$_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= $colors[$c] ? Term::ANSIColor::colored($row->[$c], $colors[$c]) : $row->[$c]; $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } return $text; } sub doc_sub_commands { my $self = shift; my ($order, $categories) = $self->_categorize_sub_commands(@_); my $text = ""; my $indent_lvl = 4; for my $category (@$order) { my $category_name = ($category ? uc $category : "GENERAL"); $text .= "=head2 $category_name\n\n"; for my $cmd (@{$categories->{$category}}) { $text .= "=over $indent_lvl\n\n"; my $name = $cmd->command_name_brief; my $link = $cmd->command_name; $link =~ s/ /-/g; my $description = $cmd->help_brief; $text .= "=item B>\n\n=over 2\n\n=item $description\n\n=back\n\n"; $text .= "=back\n\nE<10>\n\n"; } } return $text; } # # The following methods build allow a command to determine its # sub-commands, if there are any. # # This is for cases in which the Foo::Bar command delegates to # Foo::Bar::Baz, Foo::Bar::Buz or Foo::Bar::Doh, depending on its paramters. sub sub_command_dirs { my $class = shift; my $subdir = ref($class) || $class; $subdir =~ s|::|\/|g; my @dirs = grep { -d $_ } map { $_ . '/' . $subdir } @INC; return @dirs; } sub sub_command_classes { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return values %$mapping; } # For compatability with Command::V1-based callers sub is_sub_command_delegator { return scalar(shift->sub_command_classes); } sub command_tree_source_classes { # override in subclass if you want different sources my $class = shift; return $class; } sub _build_sub_command_mapping { my $class = shift; $class = ref($class) || $class; my @source_classes = $class->command_tree_source_classes; my $mapping; do { no strict 'refs'; $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; if (ref($mapping) eq 'HASH') { return $mapping; } }; for my $source_class (@source_classes) { # check if this class is valid eval{ $source_class->class; }; if ( $@ ) { warn $@; } # for My::Foo::Command::* commands and sub-trees my $subdir = $source_class; $subdir =~ s|::|\/|g; # for My::Foo::*::Command sub-trees my $source_class_above = $source_class; $source_class_above =~ s/::Command//; my $subdir2 = $source_class_above; $subdir2 =~ s|::|/|g; # check everywhere for my $lib (@INC) { my $subdir_full_path = $lib . '/' . $subdir; # find My::Foo::Command::* if (-d $subdir_full_path) { my @files = glob($subdir_full_path . '/*'); for my $file (@files) { my $basename = basename($file); $basename =~ s/.pm$// or next; my $sub_command_class_name = $source_class . '::' . $basename; my $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); unless ($sub_command_class_meta) { local $SIG{__DIE__}; local $SIG{__WARN__}; # until _use_safe is refactored to be permissive, use directly... print ">> $sub_command_class_name\n"; eval "use $sub_command_class_name"; } $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); next unless $sub_command_class_name->isa("Command"); next if $sub_command_class_meta->is_abstract; next if $sub_command_class_name eq $class; my $name = $source_class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } # find My::Foo::*::Command $subdir_full_path = $lib . '/' . $subdir2; my $pattern = $subdir_full_path . '/*/Command.pm'; my @paths = glob($pattern); for my $file (@paths) { next unless defined $file; next unless length $file; next unless -f $file; my $last_word = File::Basename::basename($file); $last_word =~ s/.pm$// or next; my $dir = File::Basename::dirname($file); my $second_to_last_word = File::Basename::basename($dir); my $sub_command_class_name = $source_class_above . '::' . $second_to_last_word . '::' . $last_word; next unless $sub_command_class_name->isa('Command'); next if $sub_command_class_name->__meta__->is_abstract; next if $sub_command_class_name eq $class; my $basename = $second_to_last_word; $basename =~ s/.pm$//; my $name = $source_class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } } return $mapping; } sub sub_command_names { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return keys %$mapping; } sub _try_command_class_named { my $self = shift; my $sub_class = join('::', @_); my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; # If it starts with a "-", then it's a command-line option # First attempt is to convert $sub_command into a camel-case module name # and just try loading it my $name_for_sub_command = join("", map { ucfirst($_) } split(/-/, $sub_command)); my @class_name_parts = (split(/::/,$class), $name_for_sub_command); my $sub_command_class = $self->_try_command_class_named(@class_name_parts); return $sub_command_class if $sub_command_class; # Remove "Command" if it's embedded in the middle and try inserting it in other places, starting at the end @class_name_parts = ( ( map { $_ eq 'Command' ? () : $_ } @class_name_parts) , 'Command'); for(my $i = $#class_name_parts; $i > 0; $i--) { $sub_command_class = $self->_try_command_class_named(@class_name_parts); return $sub_command_class if $sub_command_class; $class_name_parts[$i] = $class_name_parts[$i-1]; $class_name_parts[$i-1] = 'Command'; } # Didn't find it yet. Try exhaustively loading all the command modules under $class my $mapping = $class->_build_sub_command_mapping; if (my $sub_command_class = $mapping->{$sub_command}) { return $sub_command_class; } else { return; } } my $depth = 0; sub __extend_namespace__ { my ($self,$ext) = @_; my $meta = $self->SUPER::__extend_namespace__($ext); return $meta if $meta; $depth++; if ($depth>1) { $depth--; return; } my $class = Command::Tree::class_for_sub_command((ref $self || $self), $self->_command_name_for_class_word($ext)); return $class->__meta__ if $class; return; } 1; __END__ =pod =head1 NAME Command::Tree -base class for commands which delegate to a list of sub-commands =head1 DESCRIPTION # in Foo.pm class Foo { is => 'Command::Tree' }; # in Foo/Cmd1.pm class Foo::Cmd1 { is => 'Command' }; # in Foo/Cmd2.pm class Foo::Cmd2 { is => 'Command' }; # in the shell $ foo cmd1 cmd2 $ foo cmd1 $ foo cmd2 =cut SubCommandFactory.pm000444023532023421 725612121654174 17675 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::SubCommandFactory; use strict; use warnings; use UR; class Command::SubCommandFactory { is => 'Command::Tree', is_abstract => 1, doc => 'Base class for commands that delegate to sub-commands that may need to be dynamically created', }; sub _init_subclass { my $subclass = shift; my $meta = $subclass->__meta__; if (grep { $_ eq __PACKAGE__ } $meta->parent_class_names) { my $delegating_class_name = $subclass; eval "sub ${subclass}::_delegating_class_name { '$delegating_class_name' }"; } return 1; } sub _build_sub_command_mapping { my ($class) = @_; unless ($class->can('_sub_commands_from')) { die "Class $class does not implement _sub_commands_from()!\n" . "This method should return the namespace to use a reference " . "for defining sub-commands." } my $ref_class = $class->_sub_commands_from; my @inheritance; if ($class->can('_sub_commands_inherit_from') and defined $class->_sub_commands_inherit_from) { @inheritance = $class->_sub_commands_inherit_from(); } else { @inheritance = $class; } my $module = $ref_class; $module =~ s/::/\//g; $module .= '.pm'; my $base_path = $INC{$module}; unless ($base_path) { if (UR::Object::Type->get($ref_class)) { $base_path = $INC{$module}; } unless ($base_path) { die "Failed to find the path for ref class $ref_class!"; } } $base_path =~ s/$module//; my $ref_path = $ref_class; $ref_path =~ s/::/\//g; my $full_ref_path = $base_path . '/' . $ref_path; my @target_paths = glob("$full_ref_path/*.pm"); my @target_class_names; for my $target_path (@target_paths) { my $target = $target_path; $target =~ s#$base_path\/$ref_path/##; $target =~ s/\.pm//; my $target_base_class = $class->_target_base_class; my $target_class_name = $target_base_class . '::' . $target; my $target_meta = UR::Object::Type->get($target_class_name); next unless $target_meta; next unless $target_class_name->isa($target_base_class); push @target_class_names, $target => $target_class_name; } my %target_classes = @target_class_names; # Create a mapping of command names to command classes, and either find or # create those command classes my $mapping; for my $target (sort keys %target_classes) { my $target_class_name = $target_classes{$target}; my $command_class_name = $class . '::' . $target; my $command_module_name = $command_class_name; $command_module_name =~ s|::|/|g; $command_module_name .= '.pm'; # If the command class already exists, load it. Otherwise, create one. if (grep { -e $_ . '/' . $command_module_name } @INC) { UR::Object::Type->get($command_class_name); } else { $class->_build_sub_command($command_class_name, @inheritance); } # Created commands need to know where their parameters came from no warnings 'redefine'; eval "sub ${command_class_name}::_target_class_name { '$target_class_name' }"; use warnings; my $command_name = $class->_command_name_for_class_word($target); $mapping->{$command_name} = $command_class_name; } return $mapping; } sub _build_sub_command { my ($self, $class_name, @inheritance) = @_; class {$class_name} { is => \@inheritance, doc => '', }; return $class_name; } sub _target_base_class { return $_[0]->_sub_commands_from; } sub _target_class_name { undef } sub _sub_commands_inherit_from { undef } 1; V1.pm000444023532023421 14315512121654174 14642 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::V1; use strict; use warnings; use UR; use Data::Dumper; use File::Basename; use Getopt::Long; use Term::ANSIColor; require Text::Wrap; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', is_abstract => 1, attributes_have => [ is_input => { is => 'Boolean', is_optional => 1 }, is_output => { is => 'Boolean', is_optional => 1 }, is_param => { is => 'Boolean', is_optional => 1 }, shell_args_position => { is => 'Integer', is_optional => 1, doc => 'when set, this property is a positional argument when run from a shell' }, ], has_optional => [ is_executed => { is => 'Boolean' }, result => { is => 'Scalar', is_output => 1 }, original_command_line => { is => 'String', doc => 'null-byte separated list of command and arguments when run via execute_with_shell_params_and_exit'}, ], ); # This is changed with "local" where used in some places $Text::Wrap::columns = 100; # Required for color output eval { binmode STDOUT, ":utf8"; binmode STDERR, ":utf8"; }; # Override method in UR::Object to support error_die and error_rv_false sub validate_subscription { my $self = shift; my $subscription_property = shift; my $retval = $self->SUPER::validate_subscription($subscription_property, @_); return $retval if $retval; unless ( defined($subscription_property) and $subscription_property eq 'error_die') { $subscription_property = '(undef)' unless defined ($subscription_property); Carp::croak("Unrecognized subscription aspect '$subscription_property'"); } return 1; } sub _init_subclass { # Each Command subclass has an automatic wrapper around execute(). # This ensures it can be called as a class or instance method, # and that proper handling occurs around it. my $subclass_name = $_[0]; no strict; no warnings; if ($subclass_name->can('execute')) { # NOTE: manipulating %{ $subclass_name . '::' } directly causes ptkdb to segfault perl my $new_symbol = "${subclass_name}::_execute_body"; my $old_symbol = "${subclass_name}::execute"; *$new_symbol = *$old_symbol; undef *$old_symbol; } else { #print "no execute in $subclass_name\n"; } if($subclass_name->can('shortcut')) { my $new_symbol = "${subclass_name}::_shortcut_body"; my $old_symbol = "${subclass_name}::shortcut"; *$new_symbol = *$old_symbol; undef *$old_symbol; } return 1; } sub shortcut { my $self = shift; return unless $self->can('_shortcut_body'); my $result = $self->_shortcut_body; $self->result($result); return $result; } sub execute { # This is a wrapper for real execute() calls. # All execute() methods are turned into _execute_body at class init, # so this will get direct control when execute() is called. my $self = shift; #TODO handle calls to SUPER::execute() from another execute(). # handle calls as a class method my $was_called_as_class_method = 0; if (ref($self)) { if ($self->is_executed) { Carp::confess("Attempt to re-execute an already executed command."); } } else { # called as class method # auto-create an instance and execute it $self = $self->create(@_); return unless $self; $was_called_as_class_method = 1; } # handle __errors__ objects before execute if (my @problems = $self->__errors__) { for my $problem (@problems) { my @properties = $problem->properties; $self->error_message("Property " . join(',', map { "'$_'" } @properties) . ': ' . $problem->desc); } my $command_name = $self->command_name; $self->error_message("Please see '$command_name --help' for more information."); $self->delete() if $was_called_as_class_method; return; } my $result; eval { $result = $self->_execute_body(@_); }; my $error = $@; if ($error or not $result) { my %error_data; $error_data{die_message} = defined($error) ? $error:''; $error_data{error_message} = defined($self->error_message) ? $self->error_message:''; $error_data{error_package} = defined($self->error_package) ? $self->error_package:''; $error_data{error_file} = defined($self->error_file) ? $self->error_file:''; $error_data{error_subroutine} = defined($self->error_subroutine) ? $self->error_subroutine:''; $error_data{error_line} = defined($self->error_line) ? $self->error_line:''; $self->__signal_observers__('error_die', %error_data); die $error if $error; } $self->is_executed(1); $self->result($result); return $self if $was_called_as_class_method; return $result; } sub _execute_body { # default implementation in the base class my $self = shift; my $class = ref($self) || $self; if ($class eq __PACKAGE__) { die "The execute() method is not defined for $_[0]!"; } return 1; } # # Standard external interface for shell dispatchers # # TODO: abstract out all dispatchers for commands into a given API sub execute_with_shell_params_and_exit { # This automatically parses command-line options and "does the right thing": my $class = shift; if (@_) { die qq| No params expected for execute_with_shell_params_and_exit(). Usage: #!/usr/bin/env perl use My::Command; My::Command->execute_with_shell_params_and_exit; |; } $Command::entry_point_class ||= $class; $Command::entry_point_bin ||= File::Basename::basename($0); if ($ENV{COMP_CWORD}) { require Getopt::Complete; my @spec = $class->resolve_option_completion_spec(); my $options = Getopt::Complete::Options->new(@spec); $options->handle_shell_completion; die "error: failed to exit after handling shell completion!"; } my @argv = @ARGV; @ARGV = (); my $exit_code; eval { $exit_code = $class->_execute_with_shell_params_and_return_exit_code(@argv); UR::Context->commit or die "Failed to commit!: " . UR::Context->error_message(); }; if ($@) { $class->error_message($@); UR::Context->rollback or die "Failed to rollback changes after failed commit!!!\n"; $exit_code = 255 unless ($exit_code); } exit $exit_code; } sub _execute_with_shell_params_and_return_exit_code { my $class = shift; my @argv = @_; my $original_cmdline = join("\0",$0,@argv); # make --foo=bar equivalent to --foo bar @argv = map { ($_ =~ /^(--\w+?)\=(.*)/) ? ($1,$2) : ($_) } @argv; my ($delegate_class, $params,$error_tag_list) = $class->resolve_class_and_params_for_argv(@argv); my $rv; if ($error_tag_list and @$error_tag_list) { $class->error_message("There were problems resolving some command-line parameters:\n\t" . join("\n\t", map { my($props,$type,$desc) = @$_{'properties','type','desc'}; "Property '" . join("','",@$props) . "' ($type): $desc" } @$error_tag_list)); } else { $rv = $class->_execute_delegate_class_with_params($delegate_class,$params,$original_cmdline); } my $exit_code = $delegate_class->exit_code_for_return_value($rv); return $exit_code; } # this is called by both the shell dispatcher and http dispatcher for now sub _execute_delegate_class_with_params { my ($class, $delegate_class, $params, $original_cmdline) = @_; unless ($delegate_class) { $class->usage_message($class->help_usage_complete_text); return; } $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); $delegate_class->dump_usage_messages(1); $delegate_class->dump_debug_messages(0); if ( $delegate_class->is_sub_command_delegator && !defined($params) ) { my $command_name = $delegate_class->command_name; $delegate_class->status_message($delegate_class->help_usage_complete_text); $delegate_class->error_message("Please specify a valid sub-command for '$command_name'."); return; } if ( $params->{help} ) { $delegate_class->usage_message($delegate_class->help_usage_complete_text); return 1; } $params->{'original_command_line'} = $original_cmdline if (defined $original_cmdline); my $command_object = $delegate_class->create(%$params); unless ($command_object) { # The delegate class should have emitted an error message. # This is just in case the developer is sloppy, and the user will think the task did not fail. print STDERR "Exiting.\n"; return; } $command_object->dump_status_messages(1); $command_object->dump_warning_messages(1); $command_object->dump_error_messages(1); $command_object->dump_debug_messages(0); my $rv = $command_object->execute($params); if ($command_object->__errors__) { $command_object->delete; } return $rv; } # # Standard programmatic interface # sub create { my $class = shift; my ($rule,%extra) = $class->define_boolexpr(@_); my @params_list = $rule->params_list; my $self = $class->SUPER::create(@params_list, %extra); return unless $self; # set non-optional boolean flags to false. for my $property_meta ($self->_shell_args_property_meta) { my $property_name = $property_meta->property_name; if (!$property_meta->is_optional and !defined($self->$property_name)) { if (defined $property_meta->data_type and $property_meta->data_type =~ /Boolean/i) { $self->$property_name(0); } } } return $self; } # # Methods to override in concrete subclasses. # # Override "execute" or "_execute_body" to implement the body of the command. # See above for details of internal implementation. # By default, there are no bare arguments. sub _bare_shell_argument_names { my $self = shift; my $meta = $self->__meta__; my @ordered_names = map { $_->property_name } sort { $a->{shell_args_position} <=> $b->{shell_args_position} } grep { $_->{shell_args_position} } $self->_shell_args_property_meta(); return @ordered_names; } # Translates a true/false value from the command module's execute() # from Perl (where positive means success), to shell (where 0 means success) # Also, execute() could return a negative value; this is converted to # positive and used as the shell exit code. NOTE: This means execute() # returning 0 and -1 mean the same thing sub exit_code_for_return_value { my $self = shift; my $return_value = shift; if (! $return_value) { $return_value = 1; } elsif ($return_value < 0) { $return_value = 0 - $return_value; } else { $return_value = 0 } return $return_value; } sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } if ($self->is_sub_command_delegator) { return ""; } else { return "no description!!!: define 'doc' in $self"; } } } sub help_synopsis { my $self = shift; return ''; } sub help_detail { my $self = shift; return "!!! define help_detail() in module " . ref($self) || $self . "!"; } sub sub_command_category { return; } sub sub_command_sort_position { # override to do something besides alpha sorting by name return '9999999999 ' . $_[0]->command_name_brief; } # # Self reflection # sub is_abstract { # Override when writing an subclass which is also abstract. my $self = shift; my $class_meta = $self->__meta__; return $class_meta->is_abstract; } sub is_executable { my $self = shift; if ($self->can("_execute_body") eq __PACKAGE__->can("_execute_body")) { return; } elsif ($self->is_abstract) { return; } else { return 1; } } sub is_sub_command_delegator { my $self = shift; if (scalar($self->sub_command_dirs)) { return 1; } else { return; } } sub _time_now { # return the current time in context # this may not be the real time in selected cases shift->__context__->now; } sub color_command_name { my $text = shift; my $colored_text = []; my @COLOR_TEMPLATES = ('red', 'bold red', 'magenta', 'bold magenta'); my @parts = split(/\s+/, $text); for(my $i = 0 ; $i < @parts ; $i++ ){ push @$colored_text, ($i < @COLOR_TEMPLATES) ? Term::ANSIColor::colored($parts[$i], $COLOR_TEMPLATES[$i]) : $parts[$i]; } return join(' ', @$colored_text); } sub _base_command_class_and_extension { my $self = shift; my $class = ref($self) || $self; return ($class =~ /^(.*)::([^\:]+)$/); } sub _command_name_for_class_word { my $self = shift; my $s = shift; $s =~ s/_/-/g; $s =~ s/^([A-Z])/\L$1/; # ignore first capital because that is assumed $s =~ s/([A-Z])/-$1/g; # all other capitals prepend a dash $s =~ s/([a-zA-Z])([0-9])/$1$2/g; # treat number as begining word $s = lc($s); return $s; } sub command_name { my $self = shift; my $class = ref($self) || $self; my $prepend = ''; $DB::single = 1; if (defined($Command::entry_point_class) and $class =~ /^($Command::entry_point_class)(::.+|)$/) { $prepend = $Command::entry_point_bin; $class = $2; if ($class =~ s/^:://) { $prepend .= ' '; } } my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } @words); return $prepend . $n; } sub command_name_brief { my $self = shift; my $class = ref($self) || $self; my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } $words[-1]); return $n; } # # Methods to transform shell args into command properties # my $_resolved_params_from_get_options = {}; sub _resolved_params_from_get_options { return $_resolved_params_from_get_options; } sub resolve_option_completion_spec { my $class = shift; my @completion_spec; if ($class->is_sub_command_delegator) { my @sub = eval { $class->sub_command_names}; if ($@) { $class->warning_message("Couldn't load class $class: $@\nSkipping $class..."); return; } for my $sub (@sub) { my $sub_class = $class->class_for_sub_command($sub); my $sub_tree = $sub_class->resolve_option_completion_spec() if defined($sub_class); # Hack to fix several broken commands, this should be removed once commands are fixed. # If the commands were not broken then $sub_tree will always exist. # Basically if $sub_tree is undef then we need to remove '>' to not break the OPTS_SPEC if ($sub_tree) { push @completion_spec, '>' . $sub => $sub_tree; } else { print "WARNING: $sub has sub_class $sub_class of ($class) but could not resolve option completion spec for it.\n". "Setting $sub to non-delegating command, investigate to correct tab completion.\n"; push @completion_spec, $sub => undef; } } push @completion_spec, "help!" => undef; } else { my $params_hash; @completion_spec = $class->_shell_args_getopt_complete_specification; no warnings; unless (grep { /^help\W/ } @completion_spec) { push @completion_spec, "help!" => undef; } } return \@completion_spec } sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; if ($self->is_sub_command_delegator) { if ( $argv[0] and $argv[0] !~ /^\-/ and my $class_for_sub_command = $self->class_for_sub_command($argv[0]) ) { # delegate shift @argv; return $class_for_sub_command->resolve_class_and_params_for_argv(@argv); } if (@argv) { # this has sub-commands, and is also executable # fall through to the execution_logic... } else { #$self->error_message( # 'Bad command "' . $sub_command . '"' # , "\ncommands:" # , $self->help_sub_commands #); return ($self,undef); } } my ($params_hash,@spec) = $self->_shell_args_getopt_specification; unless (grep { /^help\W/ } @spec) { push @spec, "help!"; } # Thes nasty GetOptions modules insist on working on # the real @ARGV, while we like a little more flexibility. # Not a problem in Perl. :) (which is probably why it was never fixed) local @ARGV; @ARGV = @argv; do { # GetOptions also likes to emit warnings instead of return a list of errors :( my @errors; local $SIG{__WARN__} = sub { push @errors, @_ }; ## Change the pattern to be '--', '-' followed by a non-digit, or '+'. ## This s the effect of treating a negative number as a value of an option. ## This means that we won't be allowed to have an option named, say, -1. ## But since command modules' properties have to be allowable function names, ## and "1" is not a valid function name, it's not really a problem #Getopt::Long::Configure('prefix_pattern=--|-(?!\D)|\+'); unless (GetOptions($params_hash,@spec)) { Carp::croak( join("\n", @errors) ); } }; # Q: Is there a standard getopt spec for capturing non-option paramters? # Perhaps that's not getting "options" :) # A: Yes. Use '<>'. But we need to process this anyway, so it won't help us. if (my @names = $self->_bare_shell_argument_names) { for (my $n=0; $n < @ARGV; $n++) { my $name = $names[$n]; unless ($name) { $self->error_message("Unexpected bare arguments: @ARGV[$n..$#ARGV]!"); return($self, undef); } my $value = $ARGV[$n]; my $meta = $self->__meta__->property_meta_for_name($name); if ($meta->is_many) { if ($n == $#names) { # slurp the rest $params_hash->{$name} = [@ARGV[$n..$#ARGV]]; last; } else { die "has-many property $name is not last in bare_shell_argument_names for $self?!"; } } else { $params_hash->{$name} = $value; } } } elsif (@ARGV) { ## argv but no names $self->error_message("Unexpected bare arguments: @ARGV!"); return($self, undef); } for my $key (keys %$params_hash) { # handle any has-many comma-sep values my $value = $params_hash->{$key}; if (ref($value)) { my @new_value; for my $v (@$value) { my @parts = split(/,\s*/,$v); push @new_value, @parts; } @$value = @new_value; } elsif ($value eq q('') or $value eq q("")) { # Handle the special values '' and "" to mean undef/NULL $params_hash->{$key} = ''; } # turn dashes into underscores my $new_key = $key; next unless ($new_key =~ tr/-/_/); if (exists $params_hash->{$new_key} && exists $params_hash->{$key}) { # this corrects a problem where is_many properties badly interact # with bare args leaving two entries in the hash like: # a-bare-opt => [], a_bare_opt => ['with','vals'] delete $params_hash->{$key}; next; } $params_hash->{$new_key} = delete $params_hash->{$key}; } $_resolved_params_from_get_options = $params_hash; return $self, $params_hash; } # # Methods which let the command auto-document itself. # sub help_usage_complete_text { my $self = shift; my $command_name = $self->command_name; my $text; if (not $self->is_executable) { # no execute implemented if ($self->is_sub_command_delegator) { # show the list of sub-commands $text = sprintf( "Sub-commands for %s:\n%s", Term::ANSIColor::colored($command_name, 'bold'), $self->help_sub_commands, ); } else { # developer error my (@sub_command_dirs) = $self->sub_command_dirs; if (grep { -d $_ } @sub_command_dirs) { $text .= "No execute() implemented in $self, and no sub-commands found!" } else { $text .= "No execute() implemented in $self, and no directory of sub-commands found!" } } } else { # standard: update this to do the old --help format my $synopsis = $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0); my $optional_args = $self->help_options(is_optional => 1); my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; $text = sprintf( "\n%s\n%s\n\n%s%s%s%s%s\n", Term::ANSIColor::colored('USAGE', 'underline'), Text::Wrap::wrap( ' ', ' ', Term::ANSIColor::colored($self->command_name, 'bold'), $self->_shell_args_usage_string || '', ), ( $synopsis ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SYNOPSIS", 'underline'), $synopsis) : '' ), ( $required_args ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED ARGUMENTS", 'underline'), $required_args) : '' ), ( $optional_args ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL ARGUMENTS", 'underline'), $optional_args) : '' ), sprintf( "%s\n%s\n", Term::ANSIColor::colored("DESCRIPTION", 'underline'), Text::Wrap::wrap(' ', ' ', $self->help_detail || '') ), ( $sub_commands ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SUB-COMMANDS", 'underline'), $sub_commands) : '' ), ); } return $text; } sub doc_sections { my $self = shift; my @sections; my $command_name = $self->command_name; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $help_brief = $self->help_brief; my $datetime = $self->__context__->now; my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; my ($date,$time) = split(' ',$datetime); push(@sections, UR::Doc::Section->create( title => "NAME", content => "$command_name" . ($help_brief ? " - $help_brief" : ""), format => "pod", )); push(@sections, UR::Doc::Section->create( title => "VERSION", content => "This document " # separated to trick the version updater . "describes $command_name " . ($version ? "version $version " : "") . "($date at $time)", format => "pod", )); if ($sub_commands) { push(@sections, UR::Doc::Section->create( title => "SUB-COMMANDS", content => $sub_commands, format => 'pod', )); } else { my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; if ($synopsis) { push(@sections, UR::Doc::Section->create( title => "SYNOPSIS", content => $synopsis, format => 'pod' )); } my $required_args = $self->help_options(is_optional => 0, format => "pod"); if ($required_args) { push(@sections, UR::Doc::Section->create( title => "REQUIRED ARGUMENTS", content => "=over\n\n$required_args\n\n=back\n\n", format => 'pod' )); } my $optional_args = $self->help_options(is_optional => 1, format => "pod"); if ($optional_args) { push(@sections, UR::Doc::Section->create( title => "OPTIONAL ARGUMENTS", content => "=over\n\n$optional_args\n\n=back\n\n", format => 'pod' )); } push(@sections, UR::Doc::Section->create( title => "DESCRIPTION", content => join('', map { " $_\n" } split ("\n",$self->help_detail)), format => 'pod', )); } return @sections; } sub help_usage_command_pod { my $self = shift; my $command_name = $self->command_name; my $pod; if (0) { # (not $self->is_executable) # no execute implemented if ($self->is_sub_command_delegator) { # show the list of sub-commands $pod = "Commands:\n" . $self->help_sub_commands; } else { # developer error my (@sub_command_dirs) = $self->sub_command_dirs; if (grep { -d $_ } @sub_command_dirs) { $pod .= "No execute() implemented in $self, and no sub-commands found!" } else { $pod .= "No execute() implemented in $self, and no directory of sub-commands found!" } } } else { # standard: update this to do the old --help format my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0, format => "pod"); my $optional_args = $self->help_options(is_optional => 1, format => "pod"); my $sub_commands = $self->help_sub_commands(brief => 1) if $self->is_sub_command_delegator; my $help_brief = $self->help_brief; my $version = do { no strict; ${ $self->class . '::VERSION' } }; $pod = "\n=pod" . "\n\n=head1 NAME" . "\n\n" . $self->command_name . ($help_brief ? " - " . $self->help_brief : '') . "\n\n"; if ($version) { $pod .= "\n\n=head1 VERSION" . "\n\n" . "This document " # separated to trick the version updater . "describes " . $self->command_name . " version " . $version . '.' . "\n\n"; } if ($sub_commands) { $pod .= ( $sub_commands ? "=head1 SUB-COMMANDS\n\n" . $sub_commands . "\n\n" : '' ) } else { $pod .= ( $synopsis ? "=head1 SYNOPSIS\n\n" . $synopsis . "\n\n" : '' ) . ( $required_args ? "=head1 REQUIRED ARGUMENTS\n\n=over\n\n" . $required_args . "\n\n=back\n\n" : '' ) . ( $optional_args ? "=head1 OPTIONAL ARGUMENTS\n\n=over\n\n" . $optional_args . "\n\n=back\n\n" : '' ) . "=head1 DESCRIPTION:\n\n" . join('', map { " $_\n" } split ("\n",$self->help_detail)) . "\n"; } $pod .= "\n\n=cut\n\n"; } return "\n$pod"; } sub help_header { my $class = shift; return sprintf("%s - %-80s\n", $class->command_name ,$class->help_brief ) } sub help_options { my $self = shift; my %params = @_; my $format = delete $params{format}; my @property_meta = $self->_shell_args_property_meta(%params); my @data; my $max_name_length = 0; for my $property_meta (@property_meta) { my $param_name = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $param_name = uc($param_name); } #$param_name = "--$param_name"; my $doc = $property_meta->doc; my $valid_values = $property_meta->valid_values; unless ($doc) { # Maybe a parent class has documentation for this property eval { foreach my $ancestor_class_meta ( $property_meta->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property_meta->property_name); if ($ancestor_property_meta and $doc = $ancestor_property_meta->doc) { last; } } }; } if (!$doc) { if (!$valid_values) { $doc = "(undocumented)"; } else { $doc = ''; } } if ($valid_values) { $doc .= "\nvalid values:\n"; for my $v (@$valid_values) { $doc .= " " . $v . "\n"; $max_name_length = length($v)+2 if $max_name_length < length($v)+2; } chomp $doc; } $max_name_length = length($param_name) if $max_name_length < length($param_name); my $param_type = $property_meta->data_type || ''; if (defined($param_type) and $param_type !~ m/::/) { $param_type = ucfirst(lc($param_type)); } my $default_value = $property_meta->default_value; if (defined $default_value) { if ($param_type eq 'Boolean') { $default_value = $default_value ? "'true'" : "'false' (--no$param_name)"; } elsif ($property_meta->is_many && ref($default_value) eq 'ARRAY') { if (@$default_value) { $default_value = "('" . join("','",@$default_value) . "')"; } else { $default_value = "()"; } } else { $default_value = "'$default_value'"; } $default_value = "\nDefault value $default_value if not specified"; } push @data, [$param_name, $param_type, $doc, $default_value]; if ($param_type eq 'Boolean') { push @data, ['no'.$param_name, $param_type, "Make $param_name 'false'" ]; } } my $text = ''; for my $row (@data) { if (defined($format) and $format eq 'pod') { $text .= "\n=item " . $row->[0] . ($row->[1]? ' I<' . $row->[1] . '>' : '') . "\n\n" . $row->[2] . "\n". ($row->[3]? $row->[3] . "\n" : ''); } elsif (defined($format) and $format eq 'html') { $text .= "\n\t
" . $row->[0] . ($row->[1]? ' ' . $row->[1] . '' : '') . "
" . $row->[2] . ($row->[3]? "
" . $row->[3] : '') . "
\n"; } else { $text .= sprintf( " %s\n%s\n", Term::ANSIColor::colored($row->[0], 'bold') . " " . $row->[1], Text::Wrap::wrap( " ", # 1st line indent, " ", # all other lines indent, $row->[2], $row->[3] || '', ), ); } } return $text; } sub sorted_sub_command_classes { no warnings; my @c = shift->sub_command_classes; my @commands_with_position = map { [ $_->sub_command_sort_position, $_ ] } @c; my @sorted = sort { $a->[0] <=> $b->[0] || $a->[0] cmp $b->[0] } @commands_with_position; return map { $_->[1] } @sorted; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub help_sub_commands { my $class = shift; my %params = @_; my $command_name_method = 'command_name_brief'; #my $command_name_method = ($params{brief} ? 'command_name_brief' : 'command_name'); my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @categories; for my $sub_command_class (@sub_command_classes) { my $category = $sub_command_class->sub_command_category; $category = '' if not defined $category; next if $sub_command_class->_is_hidden_in_docs(); my $sub_commands_within_category = $categories{$category}; unless ($sub_commands_within_category) { if (defined $category and length $category) { push @categories, $category; } else { unshift @categories,''; } $sub_commands_within_category = $categories{$category} = []; } push @$sub_commands_within_category,$sub_command_class; } no warnings; local $Text::Wrap::columns = 60; my $full_text = ''; my @full_data; for my $category (@categories) { my $sub_commands_within_this_category = $categories{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, $_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= Term::ANSIColor::colored($row->[$c], $colors[$c]), $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } #$DB::single = 1; return $text; } sub _is_hidden_in_docs { return; } # # Methods which transform command properties into shell args (getopt) # sub _shell_args_property_meta { my $self = shift; my $class_meta = $self->__meta__; # Find which property metas match the rules. We have to do it this way # because just calling 'get_all_property_metas()' will product multiple matches # if a property is overridden in a child class my $rule = UR::Object::Property->define_boolexpr(@_); my %seen; my (@positional,@required,@optional); foreach my $property_meta ( $class_meta->get_all_property_metas() ) { my $property_name = $property_meta->property_name; next if $seen{$property_name}++; next unless $rule->evaluate($property_meta); next if $property_name eq 'id'; next if $property_name eq 'result'; next if $property_name eq 'is_executed'; next if $property_name eq 'original_command_line'; next if $property_name =~ /^_/; next if defined($property_meta->data_type) and $property_meta->data_type =~ /::/; next if not $property_meta->is_mutable; next if $property_meta->is_delegated; next if $property_meta->is_calculated; # next if $property_meta->{is_output}; # TODO: This was breaking the G::M::T::Annotate::TranscriptVariants annotator. This should probably still be here but temporarily roll back next if $property_meta->is_transient; next if $property_meta->is_constant; if ($property_meta->{shell_args_position}) { push @positional, $property_meta; } elsif ($property_meta->is_optional) { push @optional, $property_meta; } else { push @required, $property_meta; } } my @result; @required = map { [ $_->property_name, $_ ] } @required; @optional = map { [ $_->property_name, $_ ] } @optional; @positional = map { [ $_->{shell_args_position}, $_ ] } @positional; @result = ( (sort { $a->[0] cmp $b->[0] } @required), (sort { $a->[0] cmp $b->[0] } @optional), (sort { $a->[0] <=> $b->[0] } @positional), ); return map { $_->[1] } @result; } sub _shell_arg_name_from_property_meta { my ($self, $property_meta,$singularize) = @_; my $property_name = ($singularize ? $property_meta->singular_name : $property_meta->property_name); my $param_name = $property_name; $param_name =~ s/_/-/g; return $param_name; } sub _shell_arg_getopt_qualifier_from_property_meta { my ($self, $property_meta) = @_; my $many = ($property_meta->is_many ? '@' : ''); if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { return '!' . $many; } #elsif ($property_meta->is_optional) { # return ':s' . $many; #} else { return '=s' . $many; } } sub _shell_arg_usage_string_from_property_meta { my ($self, $property_meta) = @_; my $string = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $string = uc($string); } if ($property_meta->{shell_args_position}) { if ($property_meta->is_optional) { $string = "[$string]"; } } else { $string = "--$string"; if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { $string = "[$string]"; } else { if ($property_meta->is_many) { $string .= "=?[,?]"; } else { $string .= '=?'; } if ($property_meta->is_optional) { $string = "[$string]"; } } } return $string; } sub _shell_arg_getopt_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_arg_getopt_complete_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); my $completions = $property_meta->valid_values; if ($completions) { if (ref($completions) eq 'ARRAY') { $completions = [ @$completions ]; } } else { my $type = $property_meta->data_type; my @complete_as_files = ( 'File','FilePath','Filesystem','FileSystem','FilesystemPath','FileSystemPath', 'Text','String', ); my @complete_as_directories = ( 'Directory','DirectoryPath','Dir','DirPath', ); if (!defined($type)) { $completions = 'files'; } else { for my $pattern (@complete_as_files) { if (!$type || $type eq $pattern) { $completions = 'files'; last; } } for my $pattern (@complete_as_directories) { if ( $type && $type eq $pattern) { $completions = 'directories'; last; } } } } return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), $completions, # ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_args_getopt_specification { my $self = shift; my @getopt; my @params; for my $meta ($self->_shell_args_property_meta) { my ($spec, @params_addition) = $self->_shell_arg_getopt_specification_from_property_meta($meta); push @getopt,$spec; push @params, @params_addition; } @getopt = sort @getopt; return { @params}, @getopt; } sub _shell_args_getopt_complete_specification { my $self = shift; my @getopt; for my $meta ($self->_shell_args_property_meta) { my ($spec, $completions) = $self->_shell_arg_getopt_complete_specification_from_property_meta($meta); push @getopt, $spec, $completions; } return @getopt; } sub _shell_args_usage_string { my $self = shift; if ($self->is_executable) { return join( " ", map { $self->_shell_arg_usage_string_from_property_meta($_) } $self->_shell_args_property_meta() ); } elsif ($self->is_sub_command_delegator) { my @names = $self->sub_command_names; return "[" . join("|",@names) . "] ..." } else { return "(no execute or sub commands implemented)" } return ""; } sub _shell_args_usage_string_abbreviated { my $self = shift; if ($self->is_sub_command_delegator) { return "..."; } else { my $detailed = $self->_shell_args_usage_string; if (length($detailed) <= 20) { return $detailed; } else { return substr($detailed,0,17) . '...'; } } } # # The following methods build allow a command to determine its # sub-commands, if there are any. # # This is for cases in which the Foo::Bar command delegates to # Foo::Bar::Baz, Foo::Bar::Buz or Foo::Bar::Doh, depending on its paramters. sub sub_command_dirs { my $class = shift; my $module = ref($class) || $class; $module =~ s/::/\//g; # multiple dirs is not working quite yet #my @paths = grep { -d $_ } map { "$_/$module" } @INC; #return @paths; $module .= '.pm'; my $path = $INC{$module}; unless ($path) { return; } $path =~ s/.pm$//; unless (-d $path) { return; } return $path; } sub sub_command_classes { my $class = shift; my @paths = $class->sub_command_dirs; return unless @paths; @paths = grep { s/\.pm$// } map { glob("$_/*") } grep { -d $_ } grep { defined($_) and length($_) } @paths; return unless @paths; my @classes = grep { ($_->is_sub_command_delegator or !$_->__meta__->is_abstract) } grep { $_ and $_->isa('Command') } map { $class->class_for_sub_command($_) } map { s/_/-/g; $_ } map { basename($_) } @paths; return @classes; } sub sub_command_names { my $class = shift; my @sub_command_classes = $class->sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; my $sub_class = join("", map { ucfirst($_) } split(/-/, $sub_command)); $sub_class = $class . "::" . $sub_class; my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } # Run the given command-line with stdout and stderr redirected to /dev/null sub system_inhibit_std_out_err { my($self,$cmdline) = @_; open my $oldout, ">&STDOUT" or die "Can't dup STDOUT: $!"; open my $olderr, ">&", \*STDERR or die "Can't dup STDERR: $!"; open(STDOUT,'>/dev/null'); open(STDERR,'>/dev/null'); my $ec = system ( $cmdline ); open STDOUT, ">&", $oldout or die "Can't dup \$oldout: $!"; open STDERR, ">&", $olderr or die "Can't dup \$olderr: $!"; return $ec; } sub parent_command_class { my $class = shift; $class = ref($class) if ref($class); my @components = split("::", $class); return if @components == 1; my $parent = join("::", @components[0..$#components-1]); return $parent if $parent->can("command_name"); return; } 1; __END__ =pod =head1 NAME Command - base class for modules implementing the command pattern =head1 SYNOPSIS use TopLevelNamespace; class TopLevelNamespace::SomeObj::Command { is => 'Command', has => [ someobj => { is => 'TopLevelNamespace::SomeObj', id_by => 'some_obj_id' }, verbose => { is => 'Boolean', is_optional => 1 }, ], }; sub execute { my $self = shift; if ($self->verbose) { print "Working on id ",$self->some_obj_id,"\n"; } my $result = $someobj->do_something(); if ($self->verbose) { print "Result was $result\n"; } return $result; } sub help_brief { return 'Call do_something on a SomeObj instance'; } sub help_synopsis { return 'cmd --some_obj_id 123 --verbose'; } sub help_detail { return 'This command performs a FooBarBaz transform on a SomObj object instance by calling its do_something method.'; } # Another part of the code my $cmd = TopLevelNamespace::SomeObj::Command->create(some_obj_id => $some_obj->id); $cmd->execute(); =head1 DESCRIPTION The Command module is a base class for creating other command modules implementing the Command Pattern. These modules can be easily reused in applications or loaded and executed dynamicaly in a command-line program. Each Command subclass represents a reusable work unit. The bulk of the module's code will likely be in the execute() method. execute() will usually take only a single argument, an instance of the Command subclass. =head1 Command-line use Creating a top-level Command module called, say TopLevelNamespace::Command, and a script called tln_cmd that looks like: #!/usr/bin/perl use TopLevelNamespace; TopLevelNamespace::Command->execute_with_shell_params_and_exit(); gives you an instant command-line tool as an interface to the hierarchy of command modules at TopLevelNamespace::Command. For example: > tln_cmd foo bar --baz 1 --qux will create an instance of TopLevelNamespace::Command::Foo::Bar (if that class exists) with params baz => 1 and qux => 1, assumming qux is a boolean property, call execute() on it, and translate the return value from execute() into the appropriate notion of a shell return value, meaning that if execute() returns true in the Perl sense, then the script returns 0 - true in the shell sense. The infrastructure takes care of turning the command line parameters into parameters for create(). Params designated as is_optional are, of course, optional and non-optional parameters that are missing will generate an error. --help is an implicit param applicable to all Command modules. It generates some hopefully useful text based on the documentation in the class definition (the 'doc' attributes you can attach to a class and properties), and the strings returned by help_detail(), help_brief() and help_synopsis(). =head1 TODO This documentation needs to be fleshed out more. There's a lot of special things you can do with Command modules that isn't mentioned here yet. =cut V1.t000444023532023421 122512121654174 14420 0ustar00abrummetgsc000000000000UR-0.41/lib/Commanduse strict; use warnings; use Test::More; use above 'UR'; UR::Object::Type->define( class_name => 'Test::Command', is => ['Command::V1'], ); my $command = Test::Command->create; ok($command, 'Command Object created'); is($command->status_message('foo'),'foo','Returns message in scalar context'); my @ret = $command->status_message('foo'); is($ret[0],'foo','Returns message as first element in list context'); is($ret[1],'main','Returns package as second element in list context'); ok($ret[2] =~ /V1.t$/,'Returns file name as third element in list context'); ok($ret[3] =~ /\d+/,'Returns line number as fourth element in list context'); done_testing; Shell.pm000444023532023421 676412121654175 15370 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::Shell; use strict; use warnings; use Command::V2; class Command::Shell { is => 'Command::V2', is_abstract => 1, subclassify_by => "_shell_command_subclass", has_input => [ delegate_type => { is => 'Text', shell_args_position => 1, doc => 'the class name of the command to be executed' }, argv => { is => 'Text', is_many => 1, is_optional => 1, shell_args_position => 2, doc => 'list of command-line arguments to be translated into parameters' }, ], has_transient => [ delegate => { is => 'Command', doc => 'the command which this adaptor wraps' }, _shell_command_subclass => { calculate_from => ['delegate_type'], calculate => sub { my $delegate_type = shift; my $subclass = $delegate_type . "::Shell"; eval "$subclass->class"; if ($@) { my $new_subclass = UR::Object::Type->define( class_name => $subclass, is => __PACKAGE__ ); die "Failed to fabricate subclass $subclass!" unless $new_subclass; } return $subclass; }, }, ], has_output => [ exit_code => => { is => 'Number', doc => 'the exit code to be returned to the shell', } ], doc => 'an adaptor to create and run commands as specified from a standard command-line shell (bash)' }; sub help_synopsis { return <run("Foo",@ARGV); The run() static method will construct the appropriate Command::Shell object, have it build its delegate, run the delegate's execution method in an in-memory transaction sandbox, and capture an exit code. If the correct environment variables are set, it will respond to a bash tab-completion request, such that the "foo" script can be used as a self-completer. EOS } sub run { my $class = shift; my $delegate_type = shift; my @argv = @_; my $cmd = $class->create(delegate_type => $delegate_type, argv => \@argv); #print STDERR "created $cmd\n"; $cmd->execute; my $exit_code = $cmd->exit_code; $cmd->delete; return $exit_code; } sub execute { my $self = shift; my $delegate_type = $self->delegate_type; eval "use above '$delegate_type'"; if ($@) { die "Failure to use delegate class $delegate_type!:\n$@"; } my @argv = $self->argv; my $exit_code = $delegate_type->_cmdline_run(@argv); $self->exit_code($exit_code); return 1; } # TODO: migrate all methods in Command::V2 which live in the Command::Dispatch::Shell module to this package # Methods which address $self to get to shell-specific things still call $self # Methods which address $self to get to the underlying command should instead call $self->delegate 1; V2Deprecated.pm000444023532023421 1721312121654175 16600 0ustar00abrummetgsc000000000000UR-0.41/lib/Commandpackage Command::V2; # additional methods to dispatch from a command-line use strict; use warnings; sub sorted_sub_command_classes { no warnings; my @c = shift->sub_command_classes; my @commands_with_position = map { [ $_->sub_command_sort_position, $_ ] } @c; return map { $_->[1] } sort { ($a->[0] <=> $b->[0]) || ($a->[0] cmp $b->[0]) } @commands_with_position; } sub sorted_sub_command_names { my $class = shift; my @sub_command_classes = $class->sorted_sub_command_classes; my @sub_command_names = map { $_->command_name_brief } @sub_command_classes; return @sub_command_names; } sub sub_commands_table { my $class = shift; my @sub_command_names = $class->sorted_sub_command_names; my $max_length = 0; for (@sub_command_names) { $max_length = length($_) if ($max_length < length($_)); } $max_length ||= 79; my $col_spacer = '_'x$max_length; my $n_cols = floor(80/$max_length); my $n_rows = ceil(@sub_command_names/$n_cols); my @tb_rows; for (my $i = 0; $i < @sub_command_names; $i += $n_cols) { my $end = $i + $n_cols - 1; $end = $#sub_command_names if ($end > $#sub_command_names); push @tb_rows, [@sub_command_names[$i..$end]]; } my @col_alignment; for (my $i = 0; $i < $n_cols; $i++) { push @col_alignment, { sample => "&$col_spacer" }; } my $tb = Text::Table->new(@col_alignment); $tb->load(@tb_rows); return $tb; } sub help_sub_commands { my $class = shift; my %params = @_; my $command_name_method = 'command_name_brief'; #my $command_name_method = ($params{brief} ? 'command_name_brief' : 'command_name'); my @sub_command_classes = $class->sorted_sub_command_classes; my %categories; my @categories; for my $sub_command_class (@sub_command_classes) { my $category = $sub_command_class->sub_command_category; $category = '' if not defined $category; next if $sub_command_class->_is_hidden_in_docs(); my $sub_commands_within_category = $categories{$category}; unless ($sub_commands_within_category) { if (defined $category and length $category) { push @categories, $category; } else { unshift @categories,''; } $sub_commands_within_category = $categories{$category} = []; } push @$sub_commands_within_category,$sub_command_class; } no warnings; local $Text::Wrap::columns = 60; my $full_text = ''; my @full_data; for my $category (@categories) { my $sub_commands_within_this_category = $categories{$category}; my @data = map { my @rows = split("\n",Text::Wrap::wrap('', ' ', $_->help_brief)); chomp @rows; ( [ $_->$command_name_method, $_->_shell_args_usage_string_abbreviated, $rows[0], ], map { [ '', ' ', $rows[$_], ] } (1..$#rows) ); } @$sub_commands_within_this_category; if ($category) { # add a space between categories push @full_data, ['','',''] if @full_data; if ($category =~ /\D/) { # non-numeric categories show their category as a header $category .= ':' if $category =~ /\S/; push @full_data, [ Term::ANSIColor::colored(uc($category), 'blue'), '', '' ]; } else { # numeric categories just sort } } push @full_data, @data; } my @max_width_found = (0,0,0); for (@full_data) { for my $c (0..2) { $max_width_found[$c] = length($_->[$c]) if $max_width_found[$c] < length($_->[$c]); } } my @colors = (qw/ red bold /); my $text = ''; for my $row (@full_data) { for my $c (0..2) { $text .= ' '; $text .= Term::ANSIColor::colored($row->[$c], $colors[$c]), $text .= ' '; $text .= ' ' x ($max_width_found[$c]-length($row->[$c])); } $text .= "\n"; } #$DB::single = 1; return $text; } sub sub_command_dirs { my $class = shift; my $subdir = ref($class) || $class; $subdir =~ s|::|\/|g; my @dirs = grep { -d $_ } map { $_ . '/' . $subdir } @INC; return @dirs; } sub sub_command_classes { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return values %$mapping; } sub _build_sub_command_mapping { my $class = shift; $class = ref($class) || $class; my $mapping; do { no strict 'refs'; $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; }; unless (defined $mapping) { my $subdir = $class; $subdir =~ s|::|\/|g; for my $lib (@INC) { my $subdir_full_path = $lib . '/' . $subdir; next unless -d $subdir_full_path; my @files = glob($subdir_full_path . '/*'); next unless @files; for my $file (@files) { my $basename = basename($file); $basename =~ s/.pm$//; my $sub_command_class_name = $class . '::' . $basename; my $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); unless ($sub_command_class_meta) { local $SIG{__DIE__}; local $SIG{__WARN__}; eval "use $sub_command_class_name"; } $sub_command_class_meta = UR::Object::Type->get($sub_command_class_name); next unless $sub_command_class_name->isa("Command"); next if $sub_command_class_meta->is_abstract; my $name = $class->_command_name_for_class_word($basename); $mapping->{$name} = $sub_command_class_name; } } } return $mapping; } sub sub_command_names { my $class = shift; my $mapping = $class->_build_sub_command_mapping; return keys %$mapping; } sub class_for_sub_command { my $self = shift; my $class = ref($self) || $self; my $sub_command = shift; return if $sub_command =~ /^\-/; my $sub_class = join("", map { ucfirst($_) } split(/-/, $sub_command)); $sub_class = $class . "::" . $sub_class; my $meta = UR::Object::Type->get($sub_class); # allow in memory classes unless ( $meta ) { eval "use $sub_class;"; if ($@) { if ($@ =~ /^Can't locate .*\.pm in \@INC/) { #die "Failed to find $sub_class! $class_for_sub_command.pm!\n$@"; return; } else { my @msg = split("\n",$@); pop @msg; pop @msg; $self->error_message("$sub_class failed to compile!:\n@msg\n\n"); return; } } } elsif (my $isa = $sub_class->isa("Command")) { if (ref($isa)) { # dumb modules (Test::Class) mess with the standard isa() API if ($sub_class->SUPER::isa("Command")) { return $sub_class; } else { return; } } return $sub_class; } else { return; } } 1; 1; Test.pm000444023532023421 17212121654175 15203 0ustar00abrummetgsc000000000000UR-0.41/lib/Commanduse strict; use warnings; use UR; use Command; package Command::Test; class Command::Test{ is => 'Command', }; 1; Test000755023532023421 012121654173 14526 5ustar00abrummetgsc000000000000UR-0.41/lib/CommandTree1.pm000444023532023421 27012121654173 16160 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Testuse strict; use warnings; use UR; use Command; package Command::Test::Tree1; class Command::Test::Tree1 { is => 'Command', doc => 'more exciting operations are here' }; 1; Echo.pm000444023532023421 132012121654173 16073 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Testuse strict; use warnings; use UR; use Command; package Command::Test::Echo; class Command::Test::Echo { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'echo the input back, and die or fail if those words appear in the input', }; sub execute { my $self = shift; print "job " . $self->id . " started at " . $self->__context__->now . "\n"; print STDERR "test error!\n"; for (1..10) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Tree1000755023532023421 012121654175 15510 5ustar00abrummetgsc000000000000UR-0.41/lib/Command/TestEcho2.pm000444023532023421 110312121654172 17133 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Test/Tree1use strict; use warnings; use UR; use Command; package Command::Test::Tree1::Echo2; class Command::Test::Tree1::Echo2 { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'test command 2 to echo output', }; sub execute { my $self = shift; for (1..6) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Echo1.pm000444023532023421 110412121654175 17136 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Test/Tree1use strict; use warnings; use UR; use Command; package Command::Test::Tree1::Echo1; class Command::Test::Tree1::Echo1 { is => 'Command', has => [ in => { is => 'Text' }, out => { is => 'Text', is_output => 1, is_optional => 1 }, ], doc => 'test command 1 to echo output1', }; sub execute { my $self = shift; for (1..6) { print $self->in,"\n"; sleep 1; } if ($self->in =~ /fail/) { return; } elsif ($self->in =~ /die/) { die $self->in; } $self->out($self->in); return 1; } 1; Dispatch000755023532023421 012121654173 15346 5ustar00abrummetgsc000000000000UR-0.41/lib/CommandShell.pm000444023532023421 12351712121654173 17161 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Dispatchpackage Command::V2; # additional methods to dispatch from a command-line use strict; use warnings; # instead of tacking these methods onto general Command::V2 objects # they could be put on the Command::Shell class, which is a wrapper/adaptor Command for translating from # command-line shell to purely functional commands. # old entry point # new cmds will call Command::Shell->run("MyClass",@ARGV) # which goes straight into _cmdline_run for now... sub execute_with_shell_params_and_exit { my $class = shift; if (@_) { die "No params expected for execute_with_shell_params_and_exit()!"; } my @argv = @ARGV; @ARGV = (); my $exit_code = $class->_cmdline_run(@argv); exit $exit_code; } sub _cmdline_run { # This automatically parses command-line options and "does the right thing": # TODO: abstract out all dispatchers for commands into a given API my $class = shift; my @argv = @_; $Command::entry_point_class ||= $class; $Command::entry_point_bin ||= File::Basename::basename($0); if ($ENV{COMP_CWORD}) { require Getopt::Complete; my @spec = $class->resolve_option_completion_spec(); my $options = Getopt::Complete::Options->new(@spec); $options->handle_shell_completion; die "error: failed to exit after handling shell completion!"; } my $exit_code; eval { $exit_code = $class->_execute_with_shell_params_and_return_exit_code(@argv); my @changed_objects = ( UR::Context->all_objects_loaded('UR::Object::Ghost'), grep { $_->__changes__ } UR::Context->all_objects_loaded('UR::Object') ); # Only commit if we have things to do. my @committable_changed_objects = grep {UR::Context->resolve_data_source_for_object($_)} @changed_objects; if (@committable_changed_objects > 0) { UR::Context->commit or die "Failed to commit!: " . UR::Context->error_message(); } }; if ($@) { $class->error_message($@); UR::Context->rollback or die "Failed to rollback changes after failed commit!!!\n"; $exit_code = 255 unless ($exit_code); } return $exit_code; } sub _execute_with_shell_params_and_return_exit_code { my $class = shift; my @argv = @_; my $original_cmdline = join("\0",$0,@argv); # make --foo=bar equivalent to --foo bar @argv = map { ($_ =~ /^(--\w+?)\=(.*)/) ? ($1,$2) : ($_) } @argv; my ($delegate_class, $params, $errors) = $class->resolve_class_and_params_for_argv(@argv); my $exit_code; if ($errors and @$errors) { $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); for my $error (@$errors) { $delegate_class->error_message(join(' ', $error->property_names) . ": " . $error->desc); } $exit_code = 1; } else { my $rv = $class->_execute_delegate_class_with_params($delegate_class,$params,$original_cmdline); $exit_code = $delegate_class->exit_code_for_return_value($rv); } return $exit_code; } sub _execute_delegate_class_with_params { # this is called by both the shell dispatcher and http dispatcher for now my ($class, $delegate_class, $params, $original_cmdline) = @_; unless ($delegate_class) { $class->dump_status_messages(1); $class->dump_warning_messages(1); $class->dump_error_messages(1); $class->dump_usage_messages(1); $class->dump_debug_messages(0); $class->usage_message($class->help_usage_complete_text); return; } $delegate_class->dump_status_messages(1); $delegate_class->dump_warning_messages(1); $delegate_class->dump_error_messages(1); $delegate_class->dump_usage_messages(1); $delegate_class->dump_debug_messages(0); # FIXME There should be a better check for params that are there because they came from the # command line, and params that exist for infrastructural purposes. 'original_command_line' # won't ever be given on the command line and shouldn't count toward the next test. # maybe check the is_input properties... if ( !defined($params) ) { my $command_name = $delegate_class->command_name; $delegate_class->status_message($delegate_class->help_usage_complete_text); $delegate_class->error_message("Please specify valid params for '$command_name'."); return; } if ( $params->{help} ) { $delegate_class->usage_message($delegate_class->help_usage_complete_text); return 1; } $params->{'original_command_line'} = $original_cmdline if (defined $original_cmdline); my $command_object = $delegate_class->create(%$params); unless ($command_object) { # The delegate class should have emitted an error message. # This is just in case the developer is sloppy, and the user will think the task did not fail. print STDERR "Exiting.\n"; return; } $command_object->dump_status_messages(1); $command_object->dump_warning_messages(1); $command_object->dump_error_messages(1); $command_object->dump_debug_messages(0); my $rv = $command_object->execute($params); unless ($rv) { my $command_name = $command_object->command_name; $command_object->error_message("Please see '$command_name --help' for more information."); } if ($command_object->__errors__) { $command_object->delete; } return $rv; } sub resolve_class_and_params_for_argv { # This is used by execute_with_shell_params_and_exit, but might be used within an application. my $self = shift; my @argv = @_; my ($params_hash,@spec) = $self->_shell_args_getopt_specification; unless (grep { /^help\W/ } @spec) { push @spec, "help!"; } my @error_tags; # Thes nasty GetOptions modules insist on working on # the real @ARGV, while we like a little more flexibility. # Not a problem in Perl. :) (which is probably why it was never fixed) local @ARGV; @ARGV = @argv; do { # GetOptions also likes to emit warnings instead of return a list of errors :( my @errors; my $rv; { local $SIG{__WARN__} = sub { push @errors, @_ }; ## Change the pattern to be '--', '-' followed by a non-digit, or '+'. ## This s the effect of treating a negative number as a value of an option. ## This means that we won't be allowed to have an option named, say, -1. ## But since command modules' properties have to be allowable function names, ## and "1" is not a valid function name, it's not really a problem #Getopt::Long::Configure('prefix_pattern=--|-(?!\D)|\+'); $rv = GetOptions($params_hash,@spec); } unless ($rv) { for my $error (@errors) { $self->error_message($error); } return($self, undef); } }; # Q: Is there a standard getopt spec for capturing non-option paramters? # Perhaps that's not getting "options" :) # A: Yes. Use '<>'. But we need to process this anyway, so it won't help us. if (my @names = $self->_bare_shell_argument_names) { for (my $n=0; $n < @ARGV; $n++) { my $name = $names[$n]; unless ($name) { $self->error_message("Unexpected bare arguments: @ARGV[$n..$#ARGV]!"); return($self, undef); } my $value = $ARGV[$n]; my $meta = $self->__meta__->property_meta_for_name($name); if ($meta->is_many and $n == $#names) { # slurp the rest $params_hash->{$name} = [@ARGV[$n..$#ARGV]]; last; } else { $params_hash->{$name} = $value; } } } if (@ARGV and not $self->_bare_shell_argument_names) { ## argv but no names $self->error_message("Unexpected bare arguments: @ARGV!"); return($self, undef); } for my $key (keys %$params_hash) { # handle any has-many comma-sep values my $value = $params_hash->{$key}; if (ref($value)) { my @new_value; for my $v (@$value) { my @parts = split(/,\s*/,$v); push @new_value, @parts; } @$value = @new_value; } elsif ($value eq q('') or $value eq q("")) { # Handle the special values '' and "" to mean undef/NULL $params_hash->{$key} = ''; } # turn dashes into underscores my $new_key = $key; next unless ($new_key =~ tr/-/_/); if (exists $params_hash->{$new_key} && exists $params_hash->{$key}) { # this corrects a problem where is_many properties badly interact # with bare args leaving two entries in the hash like: # a-bare-opt => [], a_bare_opt => ['with','vals'] delete $params_hash->{$key}; next; } $params_hash->{$new_key} = delete $params_hash->{$key}; } # futher work is looking for errors, and may display them # if help is set, return now # we might have returned sooner, but having full info available # allows for dynamic help if ($params_hash->{help}) { return ($self, $params_hash); } ## my $params = $params_hash; my $class = $self->class; if (my @errors = $self->_errors_from_missing_parameters($params)) { return ($class, $params, \@errors); } unless (@_) { return ($class, $params); } # should this be moved up into the methods which are only called # directly from the shell, or is it okay everywhere in this module to # presume we're a direct cmdline call? -ssmith local $ENV{UR_COMMAND_DUMP_STATUS_MESSAGES} = 1; my @params_to_resolve = $self->_params_to_resolve($params); for my $p (@params_to_resolve) { my $param_arg_str = join(',', @{$p->{value}}); my $pmeta = $self->__meta__->property($p->{name}); my @params; eval { @params = $self->resolve_param_value_from_cmdline_text($p); }; if ($@) { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$p->{name}], desc => "Errors while resolving from $param_arg_str: $@", ); } if (@params and $params[0]) { if ($pmeta->{'is_many'}) { $params->{$p->{name}} = \@params; } else { $params->{$p->{name}} = $params[0]; } } else { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$p->{name}], desc => "Problem resolving from $param_arg_str.", ); } } if (@error_tags) { return ($class, undef, \@error_tags); } else { return ($class, $params); } } sub resolve_option_completion_spec { my $class = shift; my @completion_spec = $class->_shell_args_getopt_complete_specification; no warnings; unless (grep { /^help\W/ } @completion_spec) { push @completion_spec, "help!" => undef; } return \@completion_spec } sub _errors_from_missing_parameters { my ($self, $params) = @_; my $class_meta = $self->__meta__; my @all_property_metas = $class_meta->properties(); my @specified_property_metas = grep { exists $params->{$_->property_name} } @all_property_metas; my %specified_property_metas = map { $_->property_name => $_ } @specified_property_metas; my %set_indirectly; my @todo = @specified_property_metas; while (my $property_meta = shift @todo) { if (my $via = $property_meta->via) { if (not $property_meta->is_mutable) { my $list = $set_indirectly{$via} ||= []; push @$list, $property_meta; } unless ($specified_property_metas{$via}) { my $via_meta = $specified_property_metas{$via} = $class_meta->property($via); push @specified_property_metas, $via_meta; push @todo, $via_meta; } } elsif (my $id_by = $property_meta) { my $list = $set_indirectly{$id_by} ||= []; push @$list, $property_meta; unless ($specified_property_metas{$id_by}) { my $id_by_meta = $specified_property_metas{$id_by} = $class_meta->property($id_by); push @specified_property_metas, $id_by_meta; push @todo, $id_by_meta; } } } # TODO: this should use @all_property_metas, and filter down to is_param and is_input # This old code just ignores things inherited from a base class. # We will need to be careful fixing this because it could add checks to tools which # work currently and lead to unexpected failures. my @property_names; if (my $has = $class_meta->{has}) { @property_names = $self->_unique_elements(keys %$has); } my @property_metas = map { $class_meta->property_meta_for_name($_); } @property_names; my @error_tags; for my $property_meta (@property_metas) { my $pn = $property_meta->property_name; next if $property_meta->is_optional; next if $property_meta->implied_by; next if defined $property_meta->default_value; next if defined $params->{$pn}; next if $set_indirectly{$pn}; if (my $via = $property_meta->via) { if ($params->{$via} or $set_indirectly{$via}) { next; } } my $arg = $pn; $arg =~ s/_/-/g; $arg = "--$arg"; if ($property_meta->is_output and not $property_meta->is_input and not $property_meta->is_param) { if ($property_meta->_data_type_as_class_name->__meta__->data_source) { # outputs with a data source do not need a specification # on the cmdline to "store" them after execution next; } else { push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$pn], desc => "Output requires specified destination: " . $arg . "." ); } } else { $DB::single = 1; push @error_tags, UR::Object::Tag->create( type => 'invalid', properties => [$pn], desc => "Missing required parameter: " . $arg . "." ); } } return @error_tags; } sub _params_to_resolve { my ($self, $params) = @_; my @params_to_resolve; if ($params) { my $cmeta = $self->__meta__; my @params_will_require_verification; my @params_may_require_verification; for my $param_name (keys %$params) { my $pmeta = $cmeta->property($param_name); unless ($pmeta) { # This message was a die after a next, so I guess it isn't supposed to be fatal? $self->warning_message("No metadata for property '$param_name'"); next; } my $param_type = $pmeta->data_type; next unless($self->_can_resolve_type($param_type)); my $param_arg = $params->{$param_name}; if (my $arg_type = ref($param_arg)) { next if $arg_type eq $param_type; # param is already the right type if ($arg_type ne 'ARRAY') { $self->error_message("no handler for property '$param_name' with argument type " . ref($param_arg)); next; } } else { $param_arg = [$param_arg]; } next unless (@$param_arg); my $resolve_info = { name => $param_name, class => $param_type, value => $param_arg, }; push(@params_to_resolve, $resolve_info); my $require_user_verify = $pmeta->{'require_user_verify'}; if ( defined($require_user_verify) ) { push @params_will_require_verification, "'$param_name'" if ($require_user_verify); } else { push @params_may_require_verification, "'$param_name'"; } } my @adverbs = ('will', 'may'); my @params_adverb_require_verification = ( \@params_will_require_verification, \@params_may_require_verification, ); for (my $i = 0; $i < @adverbs; $i++) { my $adverb = $adverbs[$i]; my @param_adverb_require_verification = @{$params_adverb_require_verification[$i]}; next unless (@param_adverb_require_verification); if (@param_adverb_require_verification > 1) { $param_adverb_require_verification[-1] = 'and ' . $param_adverb_require_verification[-1]; } my $param_str = join(', ', @param_adverb_require_verification); $self->status_message($param_str . " $adverb require verification..."); } } return @params_to_resolve; } sub _can_resolve_type { my ($self, $type) = @_; return 0 unless($type); my $non_classes = 0; if (ref($type) ne 'ARRAY') { $non_classes = $type !~ m/::/; } else { $non_classes = scalar grep { ! m/::/ } @$type; } return $non_classes == 0; } sub _shell_args_property_meta { my $self = shift; my $class_meta = $self->__meta__; # Find which property metas match the rules. We have to do it this way # because just calling 'get_all_property_metas()' will product multiple matches # if a property is overridden in a child class my ($rule, %extra) = UR::Object::Property->define_boolexpr(@_); my %seen; my (@positional,@required_input,@required_param,@optional_input,@optional_param); my @property_meta = $class_meta->properties(); PROP: foreach my $property_meta (@property_meta) { my $property_name = $property_meta->property_name; next if $seen{$property_name}++; next unless $rule->evaluate($property_meta); next unless $property_meta->can("is_param") and ($property_meta->is_param or $property_meta->is_input); if (%extra) { $DB::single = 1; no warnings; for my $key (keys %extra) { if ($property_meta->$key ne $extra{$key}) { next PROP; } } } next if $property_name eq 'id'; next if $property_name eq 'result'; next if $property_name eq 'is_executed'; next if $property_name eq 'original_command_line'; next if $property_name =~ /^_/; next if $property_meta->implied_by; next if $property_meta->is_calculated; # Kept commented out from UR's Command.pm, I believe is_output is a workflow property # and not something we need to exclude (counter to the old comment below). #next if $property_meta->{is_output}; # TODO: This was breaking the G::M::T::Annotate::TranscriptVariants annotator. This should probably still be here but temporarily roll back next if $property_meta->is_transient; next if $property_meta->is_constant; if (($property_meta->is_delegated) || (defined($property_meta->data_type) and $property_meta->data_type =~ /::/)) { next unless($self->can('resolve_param_value_from_cmdline_text')); } else { next unless($property_meta->is_mutable); } if ($property_meta->{shell_args_position}) { push @positional, $property_meta; } elsif ($property_meta->is_optional) { if ($property_meta->is_input) { push @optional_input, $property_meta; } elsif ($property_meta->is_param) { push @optional_param, $property_meta; } } else { if ($property_meta->is_input) { push @required_input, $property_meta; } elsif ($property_meta->is_param) { push @required_param, $property_meta; } } } my @result; @result = ( (sort { $a->position_in_module_header cmp $b->position_in_module_header } @required_param), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @optional_param), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @required_input), (sort { $a->position_in_module_header cmp $b->position_in_module_header } @optional_input), (sort { $a->shell_args_position <=> $b->shell_args_position } @positional), ); return @result; } sub _shell_arg_name_from_property_meta { my ($self, $property_meta,$singularize) = @_; my $property_name = ($singularize ? $property_meta->singular_name : $property_meta->property_name); my $param_name = $property_name; $param_name =~ s/_/-/g; return $param_name; } sub _shell_arg_getopt_qualifier_from_property_meta { my ($self, $property_meta) = @_; my $many = ($property_meta->is_many ? '@' : ''); if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { return '!' . $many; } #elsif ($property_meta->is_optional) { # return ':s' . $many; #} else { return '=s' . $many; } } sub _shell_arg_usage_string_from_property_meta { my ($self, $property_meta) = @_; my $string = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $string = uc($string); } if ($property_meta->{shell_args_position}) { if ($property_meta->is_optional) { $string = "[$string]"; } } else { $string = "--$string"; if (defined($property_meta->data_type) and $property_meta->data_type =~ /Boolean/) { $string = "[$string]"; } else { if ($property_meta->is_many) { $string .= "=?[,?]"; } else { $string .= '=?'; } if ($property_meta->is_optional) { $string = "[$string]"; } } } return $string; } sub _shell_arg_getopt_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), #this prevents defaults from being used for is_many properties #($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_arg_getopt_complete_specification_from_property_meta { my ($self,$property_meta) = @_; my $arg_name = $self->_shell_arg_name_from_property_meta($property_meta); my $completions = $property_meta->valid_values; if ($completions) { if (ref($completions) eq 'ARRAY') { $completions = [ @$completions ]; } } else { my $type = $property_meta->data_type; my @complete_as_files = ( 'File','FilePath','Filesystem','FileSystem','FilesystemPath','FileSystemPath', 'Text','String', ); my @complete_as_directories = ( 'Directory','DirectoryPath','Dir','DirPath', ); if (!defined($type)) { $completions = 'files'; } else { for my $pattern (@complete_as_files) { if (!$type || $type eq $pattern) { $completions = 'files'; last; } } for my $pattern (@complete_as_directories) { if ( $type && $type eq $pattern) { $completions = 'directories'; last; } } } } return ( $arg_name . $self->_shell_arg_getopt_qualifier_from_property_meta($property_meta), $completions, # ($property_meta->is_many ? ($arg_name => []) : ()) ); } sub _shell_args_getopt_specification { my $self = shift; my @getopt; my @params; for my $meta ($self->_shell_args_property_meta) { my ($spec, @params_addition) = $self->_shell_arg_getopt_specification_from_property_meta($meta); push @getopt,$spec; push @params, @params_addition; } @getopt = sort @getopt; return { @params}, @getopt; } sub _shell_args_getopt_complete_specification { my $self = shift; my @getopt; for my $meta ($self->_shell_args_property_meta) { my ($spec, $completions) = $self->_shell_arg_getopt_complete_specification_from_property_meta($meta); push @getopt, $spec, $completions; } return @getopt; } sub _bare_shell_argument_names { my $self = shift; my $meta = $self->__meta__; my @ordered_names = map { $_->property_name } sort { $a->{shell_args_position} <=> $b->{shell_args_position} } grep { $_->{shell_args_position} } $self->_shell_args_property_meta(); return @ordered_names; } # # Logic to turn command-line text into objects for parameter/input values # our %ALTERNATE_FROM_CLASS = (); # This will prevent infinite loops during recursion. our %SEEN_FROM_CLASS = (); our $MESSAGE; sub resolve_param_value_from_cmdline_text { my ($self, $param_info) = @_; my $param_name = $param_info->{name}; my $param_class = $param_info->{class}; my @param_args = @{$param_info->{value}}; my $param_str = join(',', @param_args); if (ref($param_class) eq 'ARRAY') { my @param_class = @$param_class; if (@param_class > 1) { die 'Multiple data types on command arguments are not supported.'; } else { $param_class = $param_class[0]; } } my $param_resolve_message = "Resolving parameter '$param_name' from command argument '$param_str'..."; my $pmeta = $self->__meta__->property($param_name); my $require_user_verify = $pmeta->{'require_user_verify'}; my @results; my $bx = eval { UR::BoolExpr->resolve_for_string($param_class, $param_str) }; my $bx_error = $@; if ($bx) { @results = $param_class->get($bx); if (@results > 1 && !defined($require_user_verify)) { $require_user_verify = 1; } } else { for my $arg (@param_args) { %SEEN_FROM_CLASS = (); # call resolve_param_value_from_text without a via_method to "bootstrap" recursion my @arg_results = $self->resolve_param_value_from_text($arg, $param_class); if (@arg_results != 1 && !defined($require_user_verify)) { $require_user_verify = 1; } push @results, @arg_results; } } if (@results) { $self->status_message($param_resolve_message . " found " . @results); } else { if ($bx_error) { $self->status_message($bx_error); } $self->status_message($param_resolve_message . " none found."); } return unless (@results); my $limit_results_method = "_limit_results_for_$param_name"; if ( $self->can($limit_results_method) ) { @results = $self->$limit_results_method(@results); return unless (@results); } @results = $self->_unique_elements(@results); if ($require_user_verify) { if (!$pmeta->{'is_many'} && @results > 1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "'$param_name' expects only one result."; } @results = $self->_get_user_verification_for_param_value($param_name, @results); } while (!$pmeta->{'is_many'} && @results > 1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "'$param_name' expects only one result, not many!"; @results = $self->_get_user_verification_for_param_value($param_name, @results); } if (wantarray) { return @results; } elsif (not defined wantarray) { return; } elsif (@results > 1) { Carp::confess("Multiple matches found!"); } else { return $results[0]; } } sub resolve_param_value_from_text { my ($self, $param_arg, $param_class, $via_method) = @_; unless ($param_class) { $param_class = $self->class; } $SEEN_FROM_CLASS{$param_class} = 1; my @results; # try getting BoolExpr, otherwise fallback on '_resolve_param_value_from_text_by_name_or_id' parser eval { @results = $self->_resolve_param_value_from_text_by_bool_expr($param_class, $param_arg); }; Carp::croak($@) if ($@ and $@ !~ m/Not a valid BoolExpr/); if (!@results && !$@) { # no result and was valid BoolExpr then we don't want to break it apart because we # could query enormous amounts of info return; } # the first param_arg is all param_args to try BoolExpr so skip if it has commas if (!@results && $param_arg !~ /,/) { my @results_by_string; if ($param_class->can('_resolve_param_value_from_text_by_name_or_id')) { @results_by_string = $param_class->_resolve_param_value_from_text_by_name_or_id($param_arg); } else { @results_by_string = $self->_resolve_param_value_from_text_by_name_or_id($param_class, $param_arg); } push @results, @results_by_string; } # if we still don't have any values then try via alternate class if (!@results && $param_arg !~ /,/) { @results = $self->_resolve_param_value_via_related_class_method($param_class, $param_arg, $via_method); } if ($via_method) { @results = map { $_->$via_method } @results; } if (wantarray) { return @results; } elsif (not defined wantarray) { return; } elsif (@results > 1) { Carp::confess("Multiple matches found!"); } else { return $results[0]; } } sub _resolve_param_value_via_related_class_method { my ($self, $param_class, $param_arg, $via_method) = @_; my @results; my $via_class; if (exists($ALTERNATE_FROM_CLASS{$param_class})) { $via_class = $param_class; } else { for my $class (keys %ALTERNATE_FROM_CLASS) { if ($param_class->isa($class)) { if ($via_class) { $self->error_message("Found additional via_class $class but already found $via_class!"); } $via_class = $class; } } } if ($via_class) { my @from_classes = sort keys %{$ALTERNATE_FROM_CLASS{$via_class}}; while (@from_classes && !@results) { my $from_class = shift @from_classes; my @methods = @{$ALTERNATE_FROM_CLASS{$via_class}{$from_class}}; my $method; if (@methods > 1 && !$via_method && !$ENV{UR_NO_REQUIRE_USER_VERIFY}) { $self->status_message("Trying to find $via_class via $from_class...\n"); my $method_choices; for (my $i = 0; $i < @methods; $i++) { $method_choices .= ($i + 1) . ": " . $methods[$i]; $method_choices .= " [default]" if ($i == 0); $method_choices .= "\n"; } $method_choices .= (scalar(@methods) + 1) . ": none\n"; $method_choices .= "Which method would you like to use?"; my $response = $self->_ask_user_question($method_choices, 0, '\d+', 1, '#'); if ($response =~ /^\d+$/) { $response--; if ($response == @methods) { $method = undef; } elsif ($response >= 0 && $response <= $#methods) { $method = $methods[$response]; } else { $self->error_message("Response was out of bounds, exiting..."); exit; } $ALTERNATE_FROM_CLASS{$via_class}{$from_class} = [$method]; } elsif (!$response) { $self->status_message("Exiting..."); } } else { $method = $methods[0]; } unless($SEEN_FROM_CLASS{$from_class}) { #$self->debug_message("Trying to find $via_class via $from_class->$method..."); @results = eval {$self->resolve_param_value_from_text($param_arg, $from_class, $method)}; } } # END for my $from_class (@from_classes) } # END if ($via_class) return @results; } sub _resolve_param_value_from_text_by_bool_expr { my ($self, $param_class, $arg) = @_; my @results; my $bx = eval { UR::BoolExpr->resolve_for_string($param_class, $arg); }; if ($bx) { @results = $param_class->get($bx); } else { die "Not a valid BoolExpr"; } #$self->debug_message("B: $param_class '$arg' " . scalar(@results)); return @results; } sub _try_get_by_id { my ($self, $param_class, $str) = @_; my $class_meta = $param_class->__meta__; my @id_property_names = $class_meta->id_property_names; if (@id_property_names == 0) { die "Failed to determine ID property names for class ($param_class)."; } elsif (@id_property_names == 1) { my $id_data_type = $class_meta->property_meta_for_name($id_property_names[0])->_data_type_as_class_name || ''; # Validate $str, if possible, to prevent warnings from database if $str does not fit column type. if ($id_data_type->isa('UR::Value::Number')) { # Oracle's Number data type includes floats but we just use integers for numeric IDs return ($str =~ /^[+-]?\d+$/); } } return 1; } sub _resolve_param_value_from_text_by_name_or_id { my ($self, $param_class, $str) = @_; my (@results); if ($self->_try_get_by_id($param_class, $str)) { @results = eval { $param_class->get($str) }; } if (!@results && $param_class->can('name')) { @results = $param_class->get(name => $str); unless (@results) { @results = $param_class->get("name like" => "$str"); } } return @results; } sub _get_user_verification_for_param_value { my ($self, $param_name, @list) = @_; my $n_list = scalar(@list); if ($n_list > 200 && !$ENV{UR_NO_REQUIRE_USER_VERIFY}) { my $response = $self->_ask_user_question("Would you [v]iew all $n_list item(s) for '$param_name', (p)roceed, or e(x)it?", 0, '[v]|p|x', 'v'); if(!$response || $response eq 'x') { $self->status_message("Exiting..."); exit; } return @list if($response eq 'p'); } my @new_list; while (!@new_list) { @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list); } my @ids = map { $_->id } @new_list; $self->status_message("The IDs for your selection are:\n" . join(',', @ids) . "\n\n"); return @new_list; } sub _get_user_verification_for_param_value_drilldown { my ($self, $param_name, @results) = @_; my $n_results = scalar(@results); my $pad = length($n_results); # Allow an environment variable to be set to disable the require_user_verify attribute return @results if ($ENV{UR_NO_REQUIRE_USER_VERIFY}); return if (@results == 0); my @dnames = map {$_->__display_name__} grep { $_->can('__display_name__') } @results; my $max_dname_length = @dnames ? length((sort { length($b) <=> length($a) } @dnames)[0]) : 0; my @statuses = map {$_->status} grep { $_->can('status') } @results; my $max_status_length = @statuses ? length((sort { length($b) <=> length($a) } @statuses)[0]) : 0; my @results_with_display_name_and_class = map { [ $_->__display_name__, $_->class, $_ ] } @results; @results = map { $_->[2] } sort { $a->[1] cmp $b->[1] } sort { $a->[0] cmp $b->[0] } @results_with_display_name_and_class; my @classes = $self->_unique_elements(map {$_->class} @results); my $response; my @caller = caller(1); while (!$response) { $self->status_message("\n"); # TODO: Replace this with lister? for (my $i = 1; $i <= $n_results; $i++) { my $param = $results[$i - 1]; my $num = $self->_pad_string($i, $pad); my $msg = "$num:"; $msg .= ' ' . $self->_pad_string($param->__display_name__, $max_dname_length, 'suffix'); my $status = ' '; if ($param->can('status')) { $status = $param->status; } $msg .= "\t" . $self->_pad_string($status, $max_status_length, 'suffix'); $msg .= "\t" . $param->class if (@classes > 1); $self->status_message($msg); } if ($MESSAGE) { $MESSAGE = "\n" . '*'x80 . "\n" . $MESSAGE . "\n" . '*'x80 . "\n"; $self->status_message($MESSAGE); $MESSAGE = ''; } my $pretty_values = '(c)ontinue, (h)elp, e(x)it'; my $valid_values = '\*|c|h|x|[-+]?[\d\-\., ]+'; if ($caller[3] =~ /_trim_list_from_response/) { $pretty_values .= ', (b)ack'; $valid_values .= '|b'; } $response = $self->_ask_user_question("Please confirm the above items for '$param_name' or modify your selection.", 0, $valid_values, 'h', $pretty_values.', or specify item numbers to use'); if (lc($response) eq 'h' || !$self->_validate_user_response_for_param_value_verification($response)) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "Help:\n". "* Specify which elements to keep by listing them, e.g. '1,3,12' would keep\n". " items 1, 3, and 12.\n". "* Begin list with a minus to remove elements, e.g. '-1,3,9' would remove\n". " items 1, 3, and 9.\n". "* Ranges can be used, e.g. '-11-17, 5' would remove items 11 through 17 and\n". " remove item 5."; $response = ''; } } if (lc($response) eq 'x') { $self->status_message("Exiting..."); exit; } elsif (lc($response) eq 'b') { return; } elsif (lc($response) eq 'c' | $response eq '*') { return @results; } elsif ($response =~ /^[-+]?[\d\-\., ]+$/) { @results = $self->_trim_list_from_response($response, $param_name, @results); return @results; } else { die $self->error_message("Conditional exception, should not have been reached!"); } } sub _ask_user_question { my $self = shift; my $question = shift; my $timeout = shift; my $valid_values = shift || "yes|no"; my $default_value = shift || undef; my $pretty_valid_values = shift || $valid_values; $valid_values = lc($valid_values); my $input; $timeout = 60 unless(defined($timeout)); local $SIG{ALRM} = sub { print STDERR "Exiting, failed to reply to question '$question' within '$timeout' seconds.\n"; exit; }; print STDERR "\n$question\n"; print STDERR "Reply with $pretty_valid_values: "; unless ($self->_can_interact_with_user) { print STDERR "\n"; die $self->error_message("Attempting to ask user question but cannot interact with user!"); } alarm($timeout) if ($timeout); chomp($input = ); alarm(0) if ($timeout); print STDERR "\n"; if(lc($input) =~ /^$valid_values$/) { return lc($input); } elsif ($default_value) { return $default_value; } else { $self->error_message("'$input' is an invalid answer to question '$question'\n\n"); return; } } sub _validate_user_response_for_param_value_verification { my ($self, $response_text) = @_; $response_text = substr($response_text, 1) if ($response_text =~ /^[+-]/); my @response = split(/[\s\,]/, $response_text); for my $response (@response) { if ($response =~ /^[xbc*]$/) { return 1; } if ($response !~ /^(\d+)([-\.]+(\d+))?$/) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "ERROR: Invalid list provided ($response)"; return 0; } if ($3 && $1 && $3 < $1) { $MESSAGE .= "\n" if ($MESSAGE); $MESSAGE .= "ERROR: Inverted range provided ($1-$3)"; return 0; } } return 1; } sub _trim_list_from_response { my ($self, $response_text, $param_name, @list) = @_; my $method; if ($response_text =~ /^[+-]/) { $method = substr($response_text, 0, 1); $response_text = substr($response_text, 1); } else { $method = '+'; } my @response = split(/[\s\,]/, $response_text); my %indices; @indices{0..$#list} = 0..$#list if ($method eq '-'); for my $response (@response) { $response =~ /^(\d+)([-\.]+(\d+))?$/; my $low = $1; $low--; my $high = $3 || $1; $high--; die if ($high < $low); if ($method eq '+') { @indices{$low..$high} = $low..$high; } else { delete @indices{$low..$high}; } } #$self->debug_message("Indices: " . join(',', sort(keys %indices))); my @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list[sort keys %indices]); unless (@new_list) { @new_list = $self->_get_user_verification_for_param_value_drilldown($param_name, @list); } return @new_list; } sub _pad_string { my ($self, $str, $width, $pos) = @_; $str = '' if ! defined $str; my $padding = $width - length($str); $padding = 0 if ($padding < 0); if ($pos && $pos eq 'suffix') { return $str . ' 'x$padding; } else { return ' 'x$padding . $str; } } sub _can_interact_with_user { my $self = shift; if ( -t STDERR ) { return 1; } else { return 0; } } sub _unique_elements { my ($self, @list) = @_; my %seen = (); my @unique = grep { ! $seen{$_} ++ } @list; return @unique; } 1; View000755023532023421 012121654173 14521 5ustar00abrummetgsc000000000000UR-0.41/lib/CommandDocMethods.pm000444023532023421 4372612121654173 17301 0ustar00abrummetgsc000000000000UR-0.41/lib/Command/Viewpackage Command::V2; # additional methods to produce documentation, TODO: turn into a real view use strict; use warnings; use Term::ANSIColor; use Pod::Simple::Text; require Text::Wrap; # This is changed with "local" where used in some places $Text::Wrap::columns = 100; # Required for color output eval { binmode STDOUT, ":utf8"; binmode STDERR, ":utf8"; }; sub help_brief { my $self = shift; if (my $doc = $self->__meta__->doc) { return $doc; } else { my @parents = $self->__meta__->ancestry_class_metas; for my $parent (@parents) { if (my $doc = $parent->doc) { return $doc; } } return "no description!!!: define 'doc' in the class definition for " . $self->class; } } sub help_synopsis { my $self = shift; return ''; } sub help_detail { my $self = shift; return "!!! define help_detail() in module " . ref($self) || $self . "!"; } sub sub_command_category { return; } sub sub_command_sort_position { # override to do something besides alpha sorting by name return '9999999999 ' . $_[0]->command_name_brief; } # LEGACY: poorly named sub help_usage_command_pod { return shift->doc_manual(@_); } # LEGACY: poorly named sub help_usage_complete_text { shift->doc_help(@_) } sub doc_help { my $self = shift; my $command_name = $self->command_name; my $text; my $extra_help = ''; my @extra_help = $self->_additional_help_sections; while (@extra_help) { my $title = shift @extra_help || ''; my $content = shift @extra_help || ''; $extra_help .= sprintf( "%s\n\n%s\n", Term::ANSIColor::colored($title, 'underline'), _pod2txt($content) ), } # standard: update this to do the old --help format my $synopsis = $self->help_synopsis; my $required_inputs = $self->help_options(is_optional => 0, is_input => 1); my $required_params = $self->help_options(is_optional => 0, is_param => 1); my $optional_inputs = $self->help_options(is_optional => 1, is_input => 1); my $optional_params = $self->help_options(is_optional => 1, is_param => 1); $DB::single = 1; my @parts; push @parts, Term::ANSIColor::colored('USAGE', 'underline'); push @parts, Text::Wrap::wrap( ' ', ' ', Term::ANSIColor::colored($self->command_name, 'bold'), $self->_shell_args_usage_string || '', ); push @parts, ( $synopsis ? sprintf("%s\n%s\n", Term::ANSIColor::colored("SYNOPSIS", 'underline'), $synopsis) : '' ); push @parts, ( $required_inputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED INPUTS", 'underline'), $required_inputs) : '' ); push @parts, ( $required_params ? sprintf("%s\n%s\n", Term::ANSIColor::colored("REQUIRED PARAMS", 'underline'), $required_params) : '' ); push @parts, ( $optional_inputs ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL INPUTS", 'underline'), $optional_inputs) : '' ); push @parts, ( $optional_params ? sprintf("%s\n%s\n", Term::ANSIColor::colored("OPTIONAL PARAMS", 'underline'), $optional_params) : '' ); push @parts, sprintf( "%s\n%s\n", Term::ANSIColor::colored("DESCRIPTION", 'underline'), _pod2txt($self->help_detail || '') ); push @parts, ( $extra_help ? $extra_help : '' ); $text = sprintf( "\n%s\n%s\n\n%s%s%s%s%s%s%s\n", @parts ); return $text; } sub parent_command_class { my $class = shift; $class = ref($class) if ref($class); my @components = split("::", $class); return if @components == 1; my $parent = join("::", @components[0..$#components-1]); return $parent if $parent->can("command_name"); return; } sub doc_sections { my $self = shift; my @sections; my $command_name = $self->command_name; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $help_brief = $self->help_brief; my $datetime = $self->__context__->now; my ($date,$time) = split(' ',$datetime); push(@sections, UR::Doc::Section->create( title => "NAME", content => "$command_name" . ($help_brief ? " - $help_brief" : ""), format => "pod", )); push(@sections, UR::Doc::Section->create( title => "VERSION", content => "This document " # separated to trick the version updater . "describes $command_name " . ($version ? "version $version " : "") . "($date at $time)", format => "pod", )); my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; if ($synopsis) { push(@sections, UR::Doc::Section->create( title => "SYNOPSIS", content => $synopsis, format => 'pod' )); } my $required_args = $self->help_options(is_optional => 0, format => "pod"); if ($required_args) { push(@sections, UR::Doc::Section->create( title => "REQUIRED ARGUMENTS", content => "=over\n\n$required_args\n\n=back\n\n", format => 'pod' )); } my $optional_args = $self->help_options(is_optional => 1, format => "pod"); if ($optional_args) { push(@sections, UR::Doc::Section->create( title => "OPTIONAL ARGUMENTS", content => "=over\n\n$optional_args\n\n=back\n\n", format => 'pod' )); } my $manual = $self->_doc_manual_body || $self->help_detail; push(@sections, UR::Doc::Section->create( title => "DESCRIPTION", content => $manual, format => 'pod', )); my @extra_help = $self->_additional_help_sections; while (@extra_help) { my $title = shift @extra_help || ''; my $content = shift @extra_help || ''; push (@sections, UR::Doc::Section->create( title => $title, content => $content, format => 'pod' )); } if ($self->can("doc_sub_commands")) { my $sub_commands = $self->doc_sub_commands(brief => 1); if ($sub_commands) { push(@sections, UR::Doc::Section->create( title => "SUB-COMMANDS", content => $sub_commands, format => "pod", )); } } my @footer_section_methods = ( 'LICENSE' => '_doc_license', 'AUTHORS' => '_doc_authors', 'CREDITS' => '_doc_credits', 'BUGS' => '_doc_bugs', 'SEE ALSO' => '_doc_see_also' ); while (@footer_section_methods) { my $header = shift @footer_section_methods; my $method = shift @footer_section_methods; my @txt = $self->$method; next if (@txt == 0 or (@txt == 1 and not $txt[0])); my $content; if (@txt == 1) { $content = $txt[0]; } else { $content = join("\n", @txt); } push(@sections, UR::Doc::Section->create( title => $header, content => $content, format => "pod", )); } return @sections; } sub doc_sub_commands { my $self = shift; return; } sub doc_manual { my $self = shift; my $pod = $self->_doc_name_version; my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $required_args = $self->help_options(is_optional => 0, format => "pod"); my $optional_args = $self->help_options(is_optional => 1, format => "pod"); $pod .= ( $synopsis ? "=head1 SYNOPSIS\n\n" . $synopsis . "\n\n" : '' ) . ( $required_args ? "=head1 REQUIRED ARGUMENTS\n\n=over\n\n" . $required_args . "\n\n=back\n\n" : '' ) . ( $optional_args ? "=head1 OPTIONAL ARGUMENTS\n\n=over\n\n" . $optional_args . "\n\n=back\n\n" : '' ); my $manual = $self->_doc_manual_body; my $help = $self->help_detail; if ($manual or $help) { $pod .= "=head1 DESCRIPTION:\n\n"; my $txt = $manual || $help; if ($txt =~ /^\=/) { # pure POD $pod .= $manual; } else { $txt =~ s/\n/\n\n/g; $pod .= $txt; #$pod .= join('', map { " $_\n" } split ("\n",$txt)) . "\n"; } } $pod .= $self->_doc_footer(); $pod .= "\n\n=cut\n\n"; return "\n$pod"; } sub _doc_name_version { my $self = shift; my $command_name = $self->command_name; my $pod; # standard: update this to do the old --help format my $synopsis = $self->command_name . ' ' . $self->_shell_args_usage_string . "\n\n" . $self->help_synopsis; my $help_brief = $self->help_brief; my $version = do { no strict; ${ $self->class . '::VERSION' } }; my $datetime = $self->__context__->now; my ($date,$time) = split(' ',$datetime); $pod = "\n=pod" . "\n\n=head1 NAME" . "\n\n" . $self->command_name . ($help_brief ? " - " . $self->help_brief : '') . "\n\n"; $pod .= "\n\n=head1 VERSION" . "\n\n" . "This document " # separated to trick the version updater . "describes " . $self->command_name; if ($version) { $pod .= " version " . $version . " ($date at $time).\n\n"; } else { $pod .= " ($date at $time)\n\n"; } return $pod; } sub _doc_manual_body { return ''; } sub help_header { my $class = shift; return sprintf("%s - %-80s\n", $class->command_name ,$class->help_brief ) } sub help_options { my $self = shift; my %params = @_; my $format = delete $params{format}; my @property_meta = $self->_shell_args_property_meta(%params); my @data; my $max_name_length = 0; for my $property_meta (@property_meta) { my $param_name = $self->_shell_arg_name_from_property_meta($property_meta); if ($property_meta->{shell_args_position}) { $param_name = uc($param_name); } #$param_name = "--$param_name"; my $doc = $property_meta->doc; my $valid_values = $property_meta->valid_values; unless ($doc) { # Maybe a parent class has documentation for this property eval { foreach my $ancestor_class_meta ( $property_meta->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property_meta->property_name); if ($ancestor_property_meta and $doc = $ancestor_property_meta->doc) { last; } } }; } if (!$doc) { if (!$valid_values) { $doc = "(undocumented)"; } else { $doc = ''; } } if ($valid_values) { $doc .= "\nvalid values:\n"; for my $v (@$valid_values) { $doc .= " " . $v . "\n"; $max_name_length = length($v)+2 if $max_name_length < length($v)+2; } chomp $doc; } $max_name_length = length($param_name) if $max_name_length < length($param_name); my $param_type = $property_meta->data_type || ''; if (defined($param_type) and $param_type !~ m/::/) { $param_type = ucfirst(lc($param_type)); } my $default_value = $property_meta->default_value; if (defined $default_value) { if ($param_type eq 'Boolean') { $default_value = $default_value ? "'true'" : "'false' (--no$param_name)"; } elsif ($property_meta->is_many && ref($default_value) eq 'ARRAY') { if (@$default_value) { $default_value = "('" . join("','",@$default_value) . "')"; } else { $default_value = "()"; } } else { $default_value = "'$default_value'"; } $default_value = "\nDefault value $default_value if not specified"; } push @data, [$param_name, $param_type, $doc, $default_value]; } my $text = ''; for my $row (@data) { if (defined($format) and $format eq 'pod') { $text .= "\n=item " . $row->[0] . ($row->[1]? ' I<' . $row->[1] . '>' : '') . "\n\n" . $row->[2] . "\n". ($row->[3]? $row->[3] . "\n" : ''); } elsif (defined($format) and $format eq 'html') { $text .= "\n\t
" . $row->[0] . ($row->[1]? ' ' . $row->[1] . '' : '') . "
" . $row->[2] . ($row->[3]? "
" . $row->[3] : '') . "
\n"; } else { $text .= sprintf( " %s\n%s\n", Term::ANSIColor::colored($row->[0], 'bold'), # . " " . $row->[1], Text::Wrap::wrap( " ", # 1st line indent, " ", # all other lines indent, $row->[2], $row->[3] || '', ), ); } } return $text; } sub _doc_footer { my $self = shift; my $pod = ''; my @method_header_map = ( 'LICENSE' => '_doc_license', 'AUTHORS' => '_doc_authors', 'CREDITS' => '_doc_credits', 'BUGS' => '_doc_bugs', 'SEE ALSO' => '_doc_see_also' ); while (@method_header_map) { my $header = shift @method_header_map; my $method = shift @method_header_map; my @txt = $self->$method; next if (@txt == 0 or (@txt == 1 and not $txt[0])); if (@txt == 1) { my @lines = split("\n",$txt[0]); $pod .= "=head1 $header\n\n" . join(" \n", @lines) . "\n\n"; } else { $pod .= "=head1 $header\n\n" . join("\n ",@txt); $pod .= "\n\n"; } } return $pod; } sub _doc_license { return ''; } sub _doc_authors { return (); } sub _doc_credits { return ''; } sub _doc_bugs { return ''; } sub _doc_see_also { return (); } sub _shell_args_usage_string { my $self = shift; return eval { if ( $self->isa('Command::Tree') ) { return '...'; } elsif ($self->can("_execute_body") eq __PACKAGE__->can("_execute_body")) { return '(no execute!)'; } elsif ($self->__meta__->is_abstract) { return '(no sub commands!)'; } else { return join( " ", map { $self->_shell_arg_usage_string_from_property_meta($_) } $self->_shell_args_property_meta() ); } }; } sub _shell_args_usage_string_abbreviated { my $self = shift; my $detailed = $self->_shell_args_usage_string; if (length($detailed) <= 20) { return $detailed; } else { return substr($detailed,0,17) . '...'; } } sub sub_command_mapping { my ($self, $class) = @_; return if !$class; no strict 'refs'; my $mapping = ${ $class . '::SUB_COMMAND_MAPPING'}; if (ref($mapping) eq 'HASH') { return $mapping; } else { return; } }; sub command_name { my $self = shift; my $class = ref($self) || $self; my $prepend = ''; # There can be a hash in the command entry point class that maps # root level tools to classes so they can be in a different location # ...this bit of code considers that misdirection: my $entry_point_class = $Command::entry_point_class; my $mapping = $self->sub_command_mapping($entry_point_class); for my $k (%$mapping) { my $v = $mapping->{$k}; if ($v && $v eq $class) { my @words = grep { $_ ne 'Command' } split(/::/,$class); return join(' ', $self->_command_name_for_class_word($words[0]), $k); } } if (defined($entry_point_class) and $class =~ /^($entry_point_class)(::.+|)$/) { $prepend = $Command::entry_point_bin; $class = $2; if ($class =~ s/^:://) { $prepend .= ' '; } } my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } @words); return $prepend . $n; } sub command_name_brief { my $self = shift; my $class = ref($self) || $self; my @words = grep { $_ ne 'Command' } split(/::/,$class); my $n = join(' ', map { $self->_command_name_for_class_word($_) } $words[-1]); return $n; } sub color_command_name { my $text = shift; my $colored_text = []; my @COLOR_TEMPLATES = ('red', 'bold red', 'magenta', 'bold magenta'); my @parts = split(/\s+/, $text); for(my $i = 0 ; $i < @parts ; $i++ ){ push @$colored_text, ($i < @COLOR_TEMPLATES) ? Term::ANSIColor::colored($parts[$i], $COLOR_TEMPLATES[$i]) : $parts[$i]; } return join(' ', @$colored_text); } sub _base_command_class_and_extension { my $self = shift; my $class = ref($self) || $self; return ($class =~ /^(.*)::([^\:]+)$/); } sub _command_name_for_class_word { my $self = shift; my $s = shift; $s =~ s/_/-/g; $s =~ s/^([A-Z])/\L$1/; # ignore first capital because that is assumed $s =~ s/([A-Z])/-$1/g; # all other capitals prepend a dash $s =~ s/([a-zA-Z])([0-9])/$1$2/g; # treat number as begining word $s = lc($s); return $s; } sub _pod2txt { my $txt = shift; my $output = ''; my $parser = Pod::Simple::Text->new; $parser->no_errata_section(1); $parser->output_string($output); $parser->parse_string_document("=pod\n\n$txt"); return $output; } sub _additional_help_sections { return; } 1; UR000755023532023421 012121654175 12561 5ustar00abrummetgsc000000000000UR-0.41/libUtil.pm000444023532023421 3576212121654172 14223 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::Util; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION; use Cwd; use Data::Dumper; use Clone::PP; sub on_destroy(&) { my $sub = shift; unless ($sub) { Carp::confess("expected an anonymous sub!") } return bless($sub, "UR::Util::CallOnDestroy"); } # used only by the above sub # the local $@ ensures that we this does not stomp on thrown exceptions sub UR::Util::CallOnDestroy::DESTROY { local $@; shift->(); } sub d { Data::Dumper->new([@_])->Terse(1)->Indent(0)->Useqq(1)->Dump; } sub null_sub { } sub used_libs { my @extra; my @compiled_inc = UR::Util::compiled_inc(); my @perl5lib = split(':', $ENV{PERL5LIB}); map { $_ =~ s/\/+$// } (@compiled_inc, @perl5lib); # remove trailing slashes map { $_ = Cwd::abs_path($_) || $_ } (@compiled_inc, @perl5lib); for my $inc (@INC) { $inc =~ s/\/+$//; my $abs_inc = Cwd::abs_path($inc) || $inc; # should already be expanded by UR.pm next if (grep { $_ =~ /^$abs_inc$/ } @compiled_inc); next if (grep { $_ =~ /^$abs_inc$/ } @perl5lib); push @extra, $inc; } unshift @extra, ($ENV{PERL_USED_ABOVE} ? split(":", $ENV{PERL_USED_ABOVE}) : ()); map { $_ =~ s/\/+$// } @extra; # remove trailing slashes again @extra = _unique_elements(@extra); return @extra; } sub _unique_elements { my @list = @_; my %seen = (); my @unique = grep { ! $seen{$_} ++ } @list; return @unique; } sub used_libs_perl5lib_prefix { my $prefix = ""; for my $i (used_libs()) { $prefix .= "$i:"; } return $prefix; } my @compiled_inc; BEGIN { use Config; my @var_list = ( 'updatesarch', 'updateslib', 'archlib', 'privlib', 'sitearch', 'sitelib', 'sitelib_stem', 'vendorarch', 'vendorlib', 'vendorlib_stem', 'extrasarch', 'extraslib', ); for my $var_name (@var_list) { if ($var_name =~ /_stem$/ && $Config{$var_name}) { my @stem_list = (split(' ', $Config{'inc_version_list'}), ''); push @compiled_inc, map { $Config{$var_name} . "/$_" } @stem_list } else { push @compiled_inc, $Config{$var_name} if $Config{$var_name}; } } # UR locks in relative paths when loaded so instead of adding '.' we add cwd push @compiled_inc, Cwd::cwd() if (${^TAINT} == 0); map { $_ =~ s/\/+/\//g } @compiled_inc; map { $_ =~ s/\/+$// } @compiled_inc; } sub compiled_inc { return @compiled_inc; } sub deep_copy { return Clone::PP::clone($_[0]); } sub value_positions_map { my ($array) = @_; my %value_pos; for (my $pos = 0; $pos < @$array; $pos++) { my $value = $array->[$pos]; if (exists $value_pos{$value}) { die "Array has duplicate values, which cannot unambiguously be given value positions!" . Data::Dumper::Dumper($array); } $value_pos{$value} = $pos; } return \%value_pos; } sub positions_of_values { # my @pos = positions_of_values(\@unordered_crap, \@correct_order); # my @fixed = @unordered_crap[@pos]; my ($unordered_array,$ordered_array) = @_; my $map = value_positions_map($unordered_array); my @translated_positions; $#translated_positions = $#$ordered_array; for (my $pos = 0; $pos < @$ordered_array; $pos++) { my $value = $ordered_array->[$pos]; my $unordered_position = $map->{$value}; $translated_positions[$pos] = $unordered_position; } # self-test: # my @now_ordered = @$unordered_array[@translated_positions]; # unless ("@now_ordered" eq "@$ordered_array") { # Carp::confess() # } return @translated_positions; } # Get all combinations of values # input is a list of listrefs of values sub combinations_of_values { return [] unless @_; my $first_values = shift; $first_values = [ $first_values ] unless (ref($first_values) and ref($first_values) eq 'ARRAY'); my @retval; foreach my $sub_combination ( &combinations_of_values(@_) ) { foreach my $value ( @$first_values ) { push @retval, [$value, @$sub_combination]; } } return @retval; } # generate a method sub _define_method { my $class = shift; my (%opts) = @_; # create method name my $method = $opts{pkg} . '::' . $opts{property}; # determine return value type my $retval; if (defined($opts{value})) { my $refval = ref($opts{value}); $retval = ($refval) ? $refval : 'SCALAR'; } else { $retval = 'SCALAR'; } # start defining method my $substr = "sub $method { my \$self = shift; "; # set default value $substr .= "\$self->{$opts{property}} = "; my $dd = Data::Dumper->new([ $opts{value} ]); $dd->Terse(1); # do not print ``$VAR1 ='' $substr .= $dd->Dump; $substr .= " unless defined(\$self->{$opts{property}}); "; # array or scalar? if ($retval eq 'ARRAY') { if ($opts{access} eq 'rw') { # allow setting of array $substr .= "\$self->{$opts{property}} = [ \@_ ] if (\@_); "; } # add return value $substr .= "return \@{ \$self->{$opts{property}} }; "; } else { # scalar if ($opts{access} eq 'rw') { # allow setting of scalar $substr .= "\$self->{$opts{property}} = \$_[0] if (\@_); "; } # add return value $substr .= "return \$self->{$opts{property}}; "; } # end the subroutine definition $substr .= "}"; # actually define the method no warnings qw(redefine); eval($substr); if ($@) { # fatal error since this is like a failed compilation die("failed to defined method $method {$substr}:$@"); } return 1; } =pod =over =item path_relative_to $rel_path = UR::Util::path_relative_to($base, $target); Returns the pathname to $target relative to $base. If $base and $target are the same, then it returns '.'. If $target is a subdirectory of of $base, then it returns the portion of $target that is unique compared to $base. If $target is not a subdirectory of $base, then it returns a relative pathname starting with $base. =back =cut sub path_relative_to { my($base,$target) = @_; $base = Cwd::abs_path($base); $target = Cwd::abs_path($target); my @base_path_parts = split('/', $base); my @target_path_parts = split('/', $target); my $i; for ($i = 0; $i < @base_path_parts and $base_path_parts[$i] eq $target_path_parts[$i]; $i++ ) { ; } my $rel_path = '../' x (scalar(@base_path_parts) - $i) . join('/', @target_path_parts[$i .. $#target_path_parts]); $rel_path = '.' unless length($rel_path); return $rel_path; } =pod =over =item generate_readwrite_methods UR::Util->generate_readwrite_methods ( some_scalar_property => 1, some_array_property => [] ); This method generates accessor/set methods named after the keys of its hash argument. The type of function generated depends on the default value provided as the hash key value. If the hash key is a scalar, a scalar method is generated. If the hash key is a reference to an array, an array method is generated. This method does not overwrite class methods that already exist. =back =cut sub generate_readwrite_methods { my $class = shift; my %properties = @_; # get package of caller my $pkg = caller; # loop through properties foreach my $property (keys(%properties)) { # do not overwrite defined methods next if $pkg->can($property); # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'rw' ); } return 1; } =pod =over =item generate_readwrite_methods_override UR::Util->generate_readwrite_methods_override ( some_scalar_property => 1, some_array_property => [] ); Same as generate_readwrite_function except that we force the functions into the namespace even if the function is already defined =back =cut sub generate_readwrite_methods_override { my $class = shift; my %properties = @_; # get package of caller my $pkg = caller; # generate the methods for each property foreach my $property (keys(%properties)) { # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'rw' ); } return 1; } =pod =over =item generate_readonly_methods UR::Util->generate_readonly_methods ( some_scalar_property => 1, some_array_property => [] ); This method generates accessor methods named after the keys of its hash argument. The type of function generated depends on the default value provided as the hash key value. If the hash key is a scalar, a scalar method is generated. If the hash key is a reference to an array, an array method is generated. This method does not overwrite class methods that already exist. =back =cut sub generate_readonly_methods { my $class = shift; my %properties = @_; # get package of caller my ($pkg) = caller; # loop through properties foreach my $property (keys(%properties)) { # do no overwrite already defined methods next if $pkg->can($property); # create method $class->_define_method ( pkg => $pkg, property => $property, value => $properties{$property}, access => 'ro' ); } return 1; } =pod =over =item mapreduce_grep my @matches = UR::Util->map_reduce_grep { shift->some_test } @candidates; Works similar to the Perl C builtin, but in a possibly-parallel fashion. If the environment variable UR_NR_CPU is set to a number greater than one, it will fork off child processes to perform the test on slices of the input list, collect the results, and return the matching items as a list. The test function is called with a single argument, an item from the list to be tested, and should return a true of false value. =back =cut sub mapreduce_grep($&@) { my $class = shift; my $subref = shift; #$DB::single = 1; # First check fast... should we do parallel at all? if (!$ENV{'UR_NR_CPU'} or $ENV{'UR_NR_CPU'} < 2) { #return grep { $subref->($_) } @_; my @ret = grep { $subref->($_) } @_; return @ret; } my(@read_handles, @child_pids); my $cleanup = sub { foreach my $handle ( @read_handles ) { $handle->close(); } kill 'TERM', @child_pids; foreach my $pid ( @child_pids ) { waitpid($pid,0); } }; my @things_to_check = @_; my($children, $length,$parent_last); if ($ENV{'UR_NR_CPU'}) { $length = POSIX::ceil(scalar(@things_to_check) / $ENV{'UR_NR_CPU'}); $children = $ENV{'UR_NR_CPU'} - 1; } else { $children = 0; $parent_last = $#things_to_check; } # FIXME - There needs to be some code in here to disconnect datasources # Oracle in particular (maybe all DBs?), stops working right unless you # disconnect before forking my $start = $length; # First child starts checking after parent's range $parent_last = $length - 1; while ($children-- > 0) { my $pipe = IO::Pipe->new(); unless ($pipe) { Carp::carp("pipe() failed: $!\nUnable to create pipes to communicate with child processes to verify transact+ion, falling back to serial verification"); $cleanup->(); $parent_last = $#things_to_check; last; } my $pid = fork(); if ($pid) { $pipe->reader(); push @read_handles, $pipe; $start += $length; } elsif (defined $pid) { $pipe->writer(); my $last = $start + $length; $last = $#things_to_check if ($last > $#things_to_check); #my @objects = grep { $subref->($_) } @things_to_check[$start .. $last]; my @matching; for (my $i = $start; $i <= $last; $i++) { if ($subref->($things_to_check[$i])) { push @matching, $i; } } # FIXME - when there's a more general framework for passing objects between # processes, use that instead #$pipe->printf("%s\n%s\n",$_->class, $_->id) foreach @objects; $pipe->print("$_\n") foreach @matching; exit; } else { Carp::carp("fork() failed: $!\nUnable to create child processes to ver+ify transaction, falling back to seri+al verification"); $cleanup->(); $parent_last = $#things_to_check; } } my @matches = grep { $subref->($_) } @things_to_check[0 .. $parent_last]; foreach my $handle ( @read_handles ) { READ_FROM_CHILD: while(1) { my $match_idx = $handle->getline(); last READ_FROM_CHILD unless $match_idx; chomp $match_idx; push @matches, $things_to_check[$match_idx]; #my $match_class = $handle->getline(); #last READ_FROM_CHILD unless $match_class; #chomp($match_class); #my $match_id = $handle->getline(); #unless (defined $match_id) { # Carp::carp("Protocol error. Tried to get object ID for class $match_class while verifying transaction"+); # last READ_FROM_CHILD; #} #chomp($match_id); #push @objects, $match_class->get($match_id); } $handle->close(); } $cleanup->(); return @matches; } # Used in several places when printing out hash-like parameters # to the user, such as in error messages sub display_string_for_params_list { my $class = shift; my %params; if (ref($_[0]) =~ 'HASH') { %params = %{$_[0]}; } else { %params = @_; } my @strings; foreach my $key ( keys %params ) { my $val = $params{$key}; $val = defined($val) ? "'$val'" : '(undef)'; push @strings, "$key => $val"; } return join(', ', @strings); } # why isn't something like this in List::Util? # Return a list of 3 listrefs: # 0: items common to both lists # 1: items in the first list only # 2: items in the second list only sub intersect_lists { my ($m,$n) = @_; my %shared; my %monly; my %nonly; @monly{@$m} = @$m; for my $v (@$n) { if ($monly{$v}) { $shared{$v} = delete $monly{$v}; } else{ $nonly{$v} = $v; } } return ( [ values %shared ], [ values %monly ], [ values %nonly ], ); } 1; =pod =head1 NAME UR::Util - Collection of utility subroutines and methods =head1 DESCRIPTION This package contains subroutines and methods used by other parts of the infrastructure. These subs are not likely to be useful to outside code. =cut Test.pm000444023532023421 521112121654172 14167 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Test; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use Test::More; sub check_properties { my $o_list = shift; my %params = @_; my $skip = delete $params{skip}; if (%params) { die "odd params passed: " . join(" ", %params); } ok( scalar(@$o_list), "got " . scalar(@$o_list) . " objects " . " of type " . ref($o_list->[0]) ); my $cn = ref($o_list->[0]); my $c = UR::Object::Type->get($cn); ok($c, "got class meta for $cn"); my @pm = map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [ $_->property_name, $_ ] } $c->all_property_metas; ok(scalar(@pm), "got " . scalar(@pm) . " properties"); if ($skip) { $skip = { map { $_ => 1 } @$skip }; my @pm_remove; my @pm_keep; for my $p (@pm) { if ($skip->{$p->property_name}) { push @pm_remove, $p; } else { push @pm_keep, $p; } } if (@pm_remove) { note( 'skipping ' . (@pm_remove) . " properties: " . join(", ", map { $_->property_name } @pm_remove) ); @pm = @pm_keep; } } my (@v,$v, $last_property_name); for my $pm (@pm) { my $p = $pm->property_name; next if defined($last_property_name) and $p eq $last_property_name; $last_property_name = $p; my $is_mutable = $pm->is_mutable; my $is_many = $pm->is_many; my %errors; #diag($p); for my $o (@$o_list) { eval { if ($is_many) { @v = $o->$p(); if ($is_mutable) { #$o->$p([]); #$o->$p(\@v); } } else { my $v = $o->$p(); if ($is_mutable) { #$o->$p(undef); #$o->$p($v); } } }; if ($@) { my ($e) = split(/\n/,$@); my $a = $errors{$e} ||= []; push @$a, $o; } } my $msg; if (%errors) { for my $error (keys %errors) { my $objects = $errors{$error}; $msg .= 'on ' . scalar(@$objects) . ' of ' . scalar(@$o_list) . "objects: " . $error; chomp $msg; $msg .= "\n"; } } ok(!$msg, "property check: $p") or diag $msg; } } 1; ModuleBase.pm000444023532023421 5444312121654172 15323 0ustar00abrummetgsc000000000000UR-0.41/lib/UR# A base class supplying error, warning, status, and debug facilities. package UR::ModuleBase; use Sub::Name; use Sub::Install; BEGIN { use Class::Autouse; # the file above now does this, but just in case: # subsequent uses of this module w/o the special override should just do nothing... $INC{"Class/Autouse_1_99_02.pm"} = 1; $INC{"Class/Autouse_1_99_04.pm"} = 1; no strict; no warnings; # ensure that modules which inherit from this never fall into the # replaced UNIVERSAL::can/isa *can = $Class::Autouse::ORIGINAL_CAN; *isa = $Class::Autouse::ORIGINAL_ISA; } =pod =head1 NAME UR::ModuleBase - Methods common to all UR classes and object instances. =head1 DESCRIPTION This is a base class for packages, classes, and objects which need to manage basic functionality in the UR framework such as inheritance, AUTOLOAD/AUTOSUB methods, error/status/warning/etc messages. UR::ModuleBase is in the @ISA list for UR::Object, but UR::ModuleBase is not a formal UR class. =head1 METHODS =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION;; # set up module use Carp; use IO::Handle; use UR::Util; =pod =over =item C $class = $obj->class; This returns the class name of a class or an object as a string. It is exactly equivalent to: (ref($self) ? ref($self) : $self) =cut sub class { my $class = shift; $class = ref($class) if ref($class); return $class; } =pod =item C $sub_ref = $obj->super_can('func'); This method determines if any of the super classes of the C<$obj> object can perform the method C. If any one of them can, reference to the subroutine that would be called (determined using a depth-first search of the C<@ISA> array) is returned. If none of the super classes provide a method named C, C is returned. =cut sub super_can { my $class = shift; foreach my $parent_class ( $class->parent_classes ) { my $code = $parent_class->can(@_); return $code if $code; } return; } =pod =item C @classes = $obj->inheritance; This method returns a depth-first list of all the classes (packages) that the class that C<$obj> was blessed into inherits from. This order is the same order as is searched when searching for inherited methods to execute. If the class has no super classes, an empty list is returned. The C class is not returned unless explicitly put into the C<@ISA> array by the class or one of its super classes. =cut sub inheritance { my $self = $_[0]; my $class = ref($self) || $self; return unless $class; no strict; my @parent_classes = @{$class . '::ISA'}; my @ordered_inheritance; foreach my $parent_class (@parent_classes) { push @ordered_inheritance, $parent_class, ($parent_class eq 'UR' ? () : inheritance($parent_class) ); } return @ordered_inheritance; } =pod =item C MyClass->parent_classes; This returns the immediate parent class, or parent classes in the case of multiple inheritance. In no case does it follow the inheritance hierarchy as ->inheritance() does. =cut sub parent_classes { my $self = $_[0]; my $class = ref($self) || $self; no strict 'refs'; my @parent_classes = @{$class . '::ISA'}; return (wantarray ? @parent_classes : $parent_classes[0]); } =pod =item C MyModule->base_dir; This returns the base directory for a given module, in which the modules's supplemental data will be stored, such as config files and glade files, data caches, etc. It uses %INC. =cut sub base_dir { my $self = shift; my $class = ref($self) || $self; $class =~ s/\:\:/\//g; my $dir = $INC{$class . '.pm'} || $INC{$class . '.pl'}; die "Failed to find module $class in \%INC: " . Data::Dumper(%INC) unless ($dir); $dir =~ s/\.p[lm]\s*$//; return $dir; } =pod =item methods Undocumented. =cut sub methods { my $self = shift; my @methods; my %methods; my ($class, $possible_method, $possible_method_full, $r, $r1, $r2); no strict; no warnings; for $class (reverse($self, $self->inheritance())) { print "$class\n"; for $possible_method (sort grep { not /^_/ } keys %{$class . "::"}) { $possible_method_full = $class . "::" . $possible_method; $r1 = $class->can($possible_method); next unless $r1; # not implemented $r2 = $class->super_can($possible_method); next if $r2 eq $r1; # just inherited { push @methods, $possible_method_full; push @{ $methods{$possible_method} }, $class; } } } print Dumper(\%methods); return @methods; } =pod =item C return MyClass->context_return(@return_values); Attempts to return either an array or scalar based on the calling context. Will die if called in scalar context and @return_values has more than 1 element. =cut sub context_return { my $class = shift; return unless defined wantarray; return @_ if wantarray; if (@_ > 1) { my @caller = caller(1); Carp::croak("Method $caller[3] on $class called in scalar context, but " . scalar(@_) . " items need to be returned"); } return $_[0]; } =pod =back =head1 C This package impliments AUTOLOAD so that derived classes can use AUTOSUB instead of AUTOLOAD. When a class or object has a method called which is not found in the final class or any derived classes, perl checks up the tree for AUTOLOAD. We impliment AUTOLOAD at the top of the tree, and then check each class in the tree in order for an AUTOSUB method. Where a class implements AUTOSUB, it will receive a function name as its first parameter, and it is expected to return either a subroutine reference, or undef. If undef is returned then the inheritance tree search will continue. If a subroutine reference is returned it will be executed immediately with the @_ passed into AUTOLOAD. Typically, AUTOSUB will be used to generate a subroutine reference, and will then associate the subref with the function name to avoid repeated calls to AUTOLOAD and AUTOSUB. Why not use AUTOLOAD directly in place of AUTOSUB? On an object with a complex inheritance tree, AUTOLOAD is only found once, after which, there is no way to indicate that the given AUTOLOAD has failed and that the inheritance tree trek should continue for other AUTOLOADS which might impliment the given method. Example: package MyClass; our @ISA = ('UR'); ##- use UR; sub AUTOSUB { my $sub_name = shift; if ($sub_name eq 'foo') { *MyClass::foo = sub { print "Calling MyClass::foo()\n" }; return \&MyClass::foo; } elsif ($sub_name eq 'bar') { *MyClass::bar = sub { print "Calling MyClass::bar()\n" }; return \&MyClass::bar; } else { return; } } package MySubClass; our @ISA = ('MyClass'); sub AUTOSUB { my $sub_name = shift; if ($sub_name eq 'baz') { *MyClass::baz = sub { print "Calling MyClass::baz()\n" }; return \&MyClass::baz; } else { return; } } package main; my $obj = bless({},'MySubClass'); $obj->foo; $obj->bar; $obj->baz; =cut our $AUTOLOAD; sub AUTOLOAD { my $self = $_[0]; # The debugger can't see $AUTOLOAD. This is just here for debugging. my $autoload = $AUTOLOAD; $autoload =~ /(.*)::([^\:]+)$/; my $package = $1; my $function = $2; return if $function eq 'DESTROY'; unless ($package) { Carp::confess("Failed to determine package name from autoload string $autoload"); } # switch these to use Class::AutoCAN / CAN? no strict; no warnings; my @classes = grep {$_} ($self, inheritance($self) ); for my $class (@classes) { if (my $AUTOSUB = $class->can("AUTOSUB")) # FIXME The above causes hard-to-read error messages if $class isn't really a class or object ref # The 2 lines below should fix the problem, but instead make other more impoartant things not work #my $AUTOSUB = eval { $class->can('AUTOSUB') }; #if ($AUTOSUB) { { if (my $subref = $AUTOSUB->($function,@_)) { goto $subref; } } } if ($autoload and $autoload !~ /::DESTROY$/) { my $subref = \&Carp::confess; @_ = ("Can't locate object method \"$function\" via package \"$package\" (perhaps you forgot to load \"$package\"?)"); goto $subref; } } =pod =head1 MESSAGING UR::ModuleBase implements several methods for sending and storing error, warning and status messages to the user. # common usage sub foo { my $self = shift; ... if ($problem) { $self->error_message("Something went wrong..."); return; } return 1; } unless ($obj->foo) { print LOG $obj->error_message(); } =head2 Messaging Methods =over 4 =item message_types @types = UR::ModuleBase->message_types; UR::ModuleBase->message_types(@more_types); With no arguments, this method returns all the types of messages that this class handles. With arguments, it adds a new type to the list. Standard message types are error, status, warning, debug and usage. Note that the addition of new types is not fully supported/implemented yet. =back =cut my $create_subs_for_message_type; # filled in lower down my @message_types = qw(error status warning debug usage); sub message_types { my $self = shift; if (@_) { foreach my $msg_type ( @_ ) { if (! $self->can("${msg_type}_message")) { # This is a new one $create_subs_for_message_type->($self, $msg_type); push @message_types, $msg_type; } } } else { return grep { $self->can($_ . '_message') } @message_types; } } # Most defaults are false my %default_messaging_settings; $default_messaging_settings{dump_error_messages} = 1; $default_messaging_settings{dump_warning_messages} = 1; $default_messaging_settings{dump_status_messages} = 1; # # Implement error_mesage/warning_message/status_message in a way # which handles object-specific callbacks. # # Build a set of methods for getting/setting/printing error/warning/status messages # $class->dump_error_messages() Turn on/off printing the messages to STDERR # error and warnings default to on, status messages default to off # $class->queue_error_messages() Turn on/off queueing of messages # defaults to off # $class->error_message("blah"): set an error message # $class->error_message() return the last message # $class->error_messages() return all the messages that have been queued up # $class->error_messages_arrayref() return the reference to the underlying # list messages get queued to. This is the method for truncating the list # or altering already queued messages # $class->error_messages_callback() Specify a callback for when error # messages are set. The callback runs before printing or queueing, so # you can alter @_ and change the message that gets printed or queued # And then the same thing for status and warning messages =pod For each message type, several methods are created for sending and retrieving messages, registering a callback to run when messages are sent, controlling whether the messages are printed on the terminal, and whether the messages are queued up. For example, for the "error" message type, these methods are created: =over 4 =item error_message $obj->error_message("Something went wrong..."); $msg = $obj->error_message(); When called with one argument, it sends an error message to the object. The error_message_callback will be run, if one is registered, and the message will be printed to the terminal. When called with no arguments, the last message sent will be returned. If the message is C then no message is printed or queued, and the next time error_message is run as an accessor, it will return undef. =item dump_error_messages $obj->dump_error_messages(0); $flag = $obj->dump_error_messages(); Get or set the flag which controls whether messages sent via C is printed to the terminal. This flag defaults to true for warning and error messages, and false for others. =item queue_error_messages $obj->queue_error_messages(0); $flag = $obj->queue_error_messages(); Get or set the flag which control whether messages send via C are saved into a list. If true, every message sent is saved and can be retrieved with L or L. This flag defaults to false for all message types. =item error_messages_callback $obj->error_messages_callback($subref); $subref = $obj->error_messages_callback(); Get or set the callback run whenever an error_message is sent. This callback is run with two arguments: The object or class error_message() was called on, and a string containing the message. This callback is run before the message is printed to the terminal or queued into its list. The callback can modify the message (by writing to $_[1]) and affect the message that is printed or queued. If $_[1] is set to C, then no message is printed or queued, and the last recorded message is set to undef as when calling error_message with undef as the argument. =item error_messages @list = $obj->error_messages(); If the queue_error_messages flag is on, then this method returns the entire list of queued messages. =item error_messages_arrayref $listref = $obj->error_messages_arrayref(); If the queue_error_messages flag is on, then this method returns a reference to the actual list where messages get queued. This list can be manipulated to add or remove items. =item error_message_source %source_info = $obj->error_message_source Returns a hash of information about the most recent call to error_message. The key "error_message" contains the message. The keys error_package, error_file, error_line and error_subroutine contain info about the location in the code where error_message() was called. =item error_package =item error_file =item error_line =item error_subroutine These methods return the same data as $obj->error_message_source(). =back =cut our $stderr = \*STDERR; our $stdout = \*STDOUT; my %message_settings; # This sub creates the settings mutator subs for each message type # For example, when passed in 'error', it creates the subs error_messages_callback, # queue_error_messages, dump_error_messages, etc $create_subs_for_message_type = sub { my($self, $type) = @_; my $class = ref($self) ? $self->class : $self; my $save_setting = sub { my($self, $name, $val) = @_; if (ref $self) { $message_settings{ $self->class . '::' . $name . '_by_id' }->{$self->id} = $val; } else { $message_settings{ $self->class . '::' . $name } = $val; } }; my $get_setting = sub { my($self, $name) = @_; if (ref $self) { return exists($message_settings{ $self->class . '::' . $name . '_by_id' }) ? $message_settings{ $self->class . '::' . $name . '_by_id' }->{$self->id} : undef; } else { return $message_settings{ $self->class . '::' . $name }; } }; my $make_mutator = sub { my $name = shift; return sub { my $self = shift; if (@_) { # setting the value $save_setting->($self, $name, @_); } else { # getting the value my $val = $get_setting->($self, $name); if (defined $val) { return $val; } elsif (ref $self) { # called on an object and no value set, try the class return $self->class->$name(); } else { # called on a class name my @super = $self->inheritance(); foreach my $super ( @super ) { if (my $super_sub = $super->can($name)) { return $super_sub->($super); } } # None of the parent classes implement it, or there aren't # any parent classes return $default_messaging_settings{$name}; } } }; }; foreach my $base ( qw( %s_messages_callback queue_%s_messages %s_package %s_file %s_line %s_subroutine ) ) { my $method = sprintf($base, $type); my $full_name = $class . '::' . $method; my $method_subref = Sub::Name::subname $full_name => $make_mutator->($method); Sub::Install::install_sub({ code => $method_subref, into => $class, as => $method, }); } my $should_dump_messages = "dump_${type}_messages"; my $dump_mutator = $make_mutator->($should_dump_messages); my @dump_env_vars = map { $_ . uc($should_dump_messages) } ('UR_', 'UR_COMMAND_'); my $should_dump_messages_subref = Sub::Name::subname $class . '::' . $should_dump_messages => sub { my $self = shift; if (@_) { return $dump_mutator->($self, @_); } foreach my $varname ( @dump_env_vars ) { return $ENV{$varname} if (defined $ENV{$varname}); } return $dump_mutator->($self); }; Sub::Install::install_sub({ code => $should_dump_messages_subref, into => $class, as => $should_dump_messages, }); my $messages_arrayref = "${type}_messages_arrayref"; my $message_arrayref_sub = Sub::Name::subname "${class}::${messages_arrayref}" => sub { my $self = shift; my $a = $get_setting->($self, $messages_arrayref); if (! defined $a) { $save_setting->($self, $messages_arrayref, $a = []); } return $a; }; Sub::Install::install_sub({ code => $message_arrayref_sub, into => $class, as => $messages_arrayref, }); my $array_subname = "${type}_messages"; my $array_subref = Sub::Name::subname "${class}::${array_subname}" => sub { my $self = shift; my $a = $get_setting->($self, $messages_arrayref); return $a ? @$a : (); }; Sub::Install::install_sub({ code => $array_subref, into => $class, as => $array_subname, }); my $messageinfo_subname = "${type}_message_source"; my @messageinfo_keys = map { $type . $_ } qw( _message _package _file _line _subroutine ); my $messageinfo_subref = Sub::Name::subname "${class}::${messageinfo_subname}" => sub { my $self = shift; return map { $_ => $self->$_ } @messageinfo_keys; }; Sub::Install::install_sub({ code => $messageinfo_subref, into => $class, as => $messageinfo_subname, }); # usage messages go to STDOUT, others to STDERR my $default_fh = $type eq 'usage' ? \$stdout : \$stderr; my $should_queue_messages = "queue_${type}_messages"; my $check_callback = "${type}_messages_callback"; my $message_text_prefix = ($type eq 'status' or $type eq 'usage') ? '' : uc($type) . ': '; my $message_package = "${type}_package"; my $message_file = "${type}_file"; my $message_line = "${type}_line"; my $message_subroutine = "${type}_subroutine"; my $logger_subname = "${type}_message"; my $logger_subref = Sub::Name::subname "${class}::${logger_subname}" => sub { my $self = shift; foreach ( @_ ) { my $msg = $_; chomp($msg) if defined; # old-style callback registered with error_messages_callback if (my $code = $self->$check_callback()) { if (ref $code) { $code->($self, $msg); } else { $self->$code($msg); } } # New-style callback registered as an observer # Some non-UR classes inherit from UR::ModuleBase, and can't __signal if ($UR::initialized && $self->can('__signal_observers__')) { $self->__signal_observers__($logger_subname, $msg); } $save_setting->($self, $logger_subname, $msg); # If the callback set $msg to undef with "$_[1] = undef", then they didn't want the message # processed further next unless defined($msg); if (my $fh = $self->$should_dump_messages()) { $fh = $$default_fh unless (ref $fh); $fh->print($message_text_prefix . $msg . "\n"); } if ($self->$should_queue_messages()) { my $a = $self->$messages_arrayref(); push @$a, $msg; } my ($package, $file, $line, $subroutine) = caller; $self->$message_package($package); $self->$message_file($file); $self->$message_line($line); $self->$message_subroutine($subroutine); } return $get_setting->($self, $logger_subname); }; Sub::Install::install_sub({ code => $logger_subref, into => $class, as => $logger_subname, }); }; # at init time, make messaging subs for the initial message types $create_subs_for_message_type->(__PACKAGE__, $_) foreach @message_types; sub _current_call_stack { my @stack = reverse split /\n/, Carp::longmess("\t"); # Get rid of the final line from carp, showing the line number # above from which we called it. pop @stack; # Get rid any other function calls which are inside of this # package besides the first one. This allows wrappers to # get_message to look at just the external call stack. # (i.e. AUTOSUB above, set_message/get_message which called this, # and AUTOLOAD in UniversalParent) pop(@stack) while ($stack[-1] =~ /^\s*(UR::ModuleBase|UR)::/ && $stack[-2] && $stack[-2] =~ /^\s*(UR::ModuleBase|UR)::/); return \@stack; } 1; __END__ =pod =head1 SEE ALSO UR(3) =cut # $Header$ ObjectV04removed.pm000444023532023421 311512121654172 16333 0ustar00abrummetgsc000000000000UR-0.41/lib/UR=pod =head1 NAME UR::ObjectV04removed - restores changes removed in UR version 0.04 =head1 SYNOPSIS use UR::ObjectV04removed =head1 DESCRIPTION Extends the UR::Object API have methods removed in the 0.04 release. If you upgrade UR, but depend on old APIs, use this module. For version 0.xx of UR, APIs may change with each release. After 1.0, APIs will only change with major releases number increments. =cut # version 0.4 commits significant refactoring of the UR::BoolExpr API # this brings back those parts which got new names package UR::BoolExpr; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; *get_rule_template = \&template; *rule_template = \&template; *get_rule_template_and_values = \&template_and_values; *get_template_and_values = \&template_and_values; *get_values = \&values; *get_underlying_rules = \&underlying_rules; *specifies_value_for_property_name = \&specifies_value_for; *specified_operator_for = \&operator_for; *specified_operator_for_propety_name = \&operator_for; *specified_value_for_id = \&value_for_id; *specified_value_for_position = \&value_for_position; *specified_value_for_property_name = \&value_for; *create_from_filter_string = \&resolve_for_string; *create_from_command_line_format_filters = \&_resolve_from_filter_array; *create_from_filters = \&_resolve_from_filter_array; *create_from_subject_class_name_keys_and_values = \&_resolve_from_subject_class_name_keys_and_values; *resolve_normalized_rule_for_class_and_params = \&resolve_normalized; *resolve_for_class_and_params = \&resolve; *get_normalized_rule_equivalent = \&normalize; Singleton.pm000444023532023421 1456212121654172 15243 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::Singleton; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Singleton', is => ['UR::Object'], is_abstract => 1, ); sub _init_subclass { my $class_name = shift; my $class_meta_object = $class_name->__meta__; # Write into the class's namespace the correct singleton overrides # to standard UR::Object methods. my $src; if ($class_meta_object->is_abstract) { $src = qq|sub ${class_name}::_singleton_object { Carp::confess("${class_name} is an abstract singleton! Select a concrete sub-class.") }| . "\n" . qq|sub ${class_name}::_singleton_class_name { Carp::confess("${class_name} is an abstract singleton! Select a concrete sub-class.") }| . "\n" . qq|sub ${class_name}::_load { shift->_abstract_load(\@_) }| } else { $src = qq|sub ${class_name}::_singleton_object { \$${class_name}::singleton or shift->_concrete_load() }| . "\n" . qq|sub ${class_name}::_singleton_class_name { '${class_name}' }| . "\n" . qq|sub ${class_name}::_load { shift->_concrete_load(\@_) }| . "\n" . qq|sub ${class_name}::get { shift->_concrete_get(\@_) }| . "\n" . qq|sub ${class_name}::is_loaded { shift->_concrete_is_loaded(\@_) }| ; } eval $src; Carp::confess($@) if $@; return 1; } # Abstract singletons havd a different load() method than concrete ones. # We could do this with forking logic, but since many of the concrete methods # get non-default handling, it's more efficient to do it this way. sub _abstract_load { my $class = shift; my $bx = $class->define_boolexpr(@_); my $id = $bx->value_for_id; unless (defined $id) { use Data::Dumper; my $params = { $bx->params_list }; Carp::confess("Cannot load a singleton ($class) except by specific identity. " . Dumper($params)); } my $subclass_name = $class->_resolve_subclass_name_for_id($id); eval "use $subclass_name"; if ($@) { undef $@; return; } return $subclass_name->get(); } # Concrete singletons have overrides to the most basic acccessors to # accomplish class/object duality smoothly. sub _concrete_get { if (@_ == 1 or (@_ == 2 and $_[0] eq $_[1])) { my $self = $_[0]->_singleton_object; return $self if $self; } return shift->_concrete_load(@_); } sub _concrete_is_loaded { if (@_ == 1 or (@_ == 2 and $_[0] eq $_[1])) { my $self = $_[0]->_singleton_object; return $self if $self; } return shift->SUPER::is_loaded(@_); } sub _concrete_load { my $class = shift; $class = ref($class) || $class; no strict 'refs'; my $varref = \${ $class . "::singleton" }; unless ($$varref) { my $id = $class->_resolve_id_for_subclass_name($class); my $class_object = $class->__meta__; my @prop_names = $class_object->all_property_names; my %default_values; foreach my $prop_name ( @prop_names ) { my $prop = $class_object->property_meta_for_name($prop_name); next unless $prop; my $val = $prop->{'default_value'}; next unless defined $val; $default_values{$prop_name} = $val; } $$varref = $UR::Context::current->_construct_object($class,%default_values, id => $id); $$varref->{db_committed} = { %$$varref }; $$varref->__signal_change__("load"); Scalar::Util::weaken($$varref); } my $self = $class->_concrete_is_loaded(@_); return unless $self; unless ($self->init) { Carp::confess("Failed to initialize singleton $class!"); } return $self; } # This is implemented in the singleton to do any post-load processing. sub init { return 1; } # All singletons require special deletion logic since they keep a #weakened reference to the singleton. sub delete { my $self = shift; my $class = $self->class; $self->SUPER::delete(); no strict 'refs'; ${ $class . "::singleton" } = undef if ${ $class . "::singleton" } eq $self; return $self; } # In most cases, the id is the class name itself, but this is not necessary. sub _resolve_subclass_name_for_id { my $class = shift; my $id = shift; return $id; } sub _resolve_id_for_subclass_name { my $class = shift; my $subclass_name = shift; return $subclass_name; } sub create { my $class = shift; my $bx = $class->define_boolexpr(@_); my $id = $bx->value_for_id; unless (defined $id) { use Data::Dumper; my $params = { $bx->params_list }; Carp::confess("No singleton ID class specified for constructor?"); } my $subclass = $class->_resolve_subclass_name_for_id($id); eval "use $subclass"; unless ($subclass->isa(__PACKAGE__)) { eval '@' . $subclass . "::ISA = ('" . __PACKAGE__ . "')"; } return $subclass->SUPER::create(@_); } 1; =pod =head1 NAME UR::Singleton - Abstract class for implementing singleton objects =head1 SYNOPSIS package MyApp::SomeClass; use UR; class MyApp::SomeClass { is => 'UR::Singleton', has => [ foo => { is => 'Number' }, ] }; $obj = MyApp::SomeClass->get(); $obj->foo(1); =head1 DESCRIPTION This class provides the infrastructure for singleton classes. Singletons are classes of which there can only be one instance, and that instance's ID is the class name. If a class inherits from UR::Singleton, it overrides the default implementation of C and C in UR::Object with code that fabricates an appropriate object the first time it's needed. Singletons are most often used as one of the parent classes for data sources within a Namespace. This makes it convienent to refer to them using only their name, as in a class definition. =head1 METHODS =over 4 =item _singleton_object $obj = Class::Name->_singleton_object; $obj = $obj->_singleton_object; Returns the object instance whether it is called as a class or object method. =item _singleton_class_name $class_name = Class::Name->_singleton_class_name; $class_name = $obj->_singleton_class_name; Returns the class name whether it is called as a class or object method. =back =head1 SEE ALSO UR::Object =cut Env.pod000444023532023421 1211112121654172 14163 0ustar00abrummetgsc000000000000UR-0.41/lib/UR=pod =head1 NAME UR::Env - Environment variables that control UR behavior =head1 DESCRIPTION UR uses several environment variables to change its behavior or provide additional debugging information. =over 4 =item UR_STACK_DUMP_ON_DIE When true, has the effect of turning any die() into a Carp::confess, meaning a stack dump will be printed after the die message. =item UR_STACK_DUMP_ON_WARN When true, has the effect of turning any warn() into a Carp::cluck, meaning a stack dump will be printed after the warn message. =item UR_CONTEXT_ROOT The name of the Root context to instantiate when the program initializes. The default is UR::Context::DefaultRoot. Other Root Contexts can be used, for example, to connect to alternate databases when running in test mode. =item UR_CONTEXT_BASE This value only changes in a sub-process which goes to its parent process for object I/O instead of the root (which is the default value for the base context in an application). =item UR_CONTEXT_CACHE_SIZE_HIGHWATER Set the object count highwater mark for the object cache pruner. See also L =item UR_CONTEXT_CACHE_SIZE_LOWWATER Set the object count lowwater mark for the object cache pruner. See also L =item UR_DEBUG_OBJECT_RELEASE When true, messages will be printed to STDERR whenever objects are removed from the object cache, such as when the object pruner marks them for removal, when they are garbage collected, unloaded, or deleted. =item UR_DEBUG_OBJECT_RELEASE When true, messages will be printed to STDERR whenever the object pruner finishes its work, and show how many objects of each class were marked for removal. =item UR_CONTEXT_MONITOR_QUERY When true (non-zero), messages will be printed as the Context satisfies queries, such as when get() is called on a class, or while processing an iterator created through SomeClass->create_iterator and iterator->next(). If the value is 1, then only queries about Non-UR classes are printed. If 2, then all queries' information is printed. =item UR_DBI_MONITOR_SQL If this is true, most interactions with data sources such as connecting, disconnecting and querying will print messages to STDERR. Same as Cmonitor_sql()>. Note that this affects non-DBI data sources as well, such as file-based data sources, which will render file I/O information instead of SQL. =item UR_DBI_SUMMARIZE_SQL If true, a report will be printed to STDERR as the program finishes about what SQL queries have been done during the program's execution, and how many times they were executed. This is helpful during optimization. =item UR_DBI_MONITOR_EVERY_FETCH Used in conjunction with UR_DBI_MONITOR_SQL, tells the data sources to also print messages to STDERR for each row fetched from the underlying data source. Same as Cmonitor_every_fetch()>. =item UR_DBI_DUMP_STACK_ON_CONNECT Print a message to STDERR only when connecting to an underlying data source. Same as Cdump_stack_on_connect()> =item UR_DBI_EXPLAIN_SQL_MATCH If the query to a data source matches the given string (interpreted as a regex), then it will attempt to do an "explain plan" and print the results before executing the query. Same as Cexplain_sql_match()> =item UR_DBI_EXPLAIN_SQL_SLOW If the time between a prepare and the first fetch of a query is longer than the given number of seconds, then it will do an "explain plan" and print the results. Same as Cexplain_sql_slow()> =item UR_DBI_EXPLAIN_SQL_CALLSTACK Used in conjunction with UR_DBI_EXPLAIN_SQL_MATCH and UR_DBI_EXPLAIN_SQL_SLOW, prints a stack trace with Carp::longmess. Same as Cexplain_sql_callstack()> =item UR_DBI_MONITOR_DML Like UR_DBI_MONITOR_SQL, but only prints information during data-altering statements, like INSERT, UPDATE or DELETE. Same as Cmonitor_dml()> =item UR_DBI_NO_COMMIT If true, data source commits will be ignored. Note that saving still occurs. If you are working with a RDBMS database, this means During UR::Context->commit(), the insert, update and delete SQL statements will be issued, but the changes will not be committed. Useful for testing. Same as Cno_commit()> =item UR_USE_DUMMY_AUTOGENERATED_IDS If true, objects created without ID params will use a special algorithm to generate IDs. Objects with these special IDs will never be saved to a data source. Useful during testing. Same as Cuse_dummy_autogenerated_ids> =item UR_USED_LIBS If true, prints a message to STDERR with the contents of @INC just before the program exits. =item UR_USED_MODS If true, prints a message to STDERR with the keys of %INC just before the program exits. This will be a list of what modules had been loaded during the life of the program. If UR_USED_MODS is greater than 1, then it will show the key/value pairs of %INC, which will show the path each module was loaded from. =back =cut ObjectV001removed.pm000444023532023421 671512121654173 16422 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Object; =pod =head1 NAME UR::ObjectV001removed - restores changes removed in UR version 0.01 =head1 SYNOPSIS use UR::ObjectV001removed =head1 DESCRIPTION Extends the UR::Object API have methods removed in the 0.01 release. If you upgrade UR, but depend on old APIs, use this module. For version 0.xx of UR, APIs may change with each release. After 1.0, APIs will only change with major releases number increments. =cut use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION; use Data::Dumper; use Scalar::Util qw(blessed); *get_class_meta = sub { shift->__meta__ }; *get_class_object = sub { shift->__meta__ }; *get_rule_for_params = \&define_boolexpr; *get_boolexpr_for_params = \&define_boolexpr; *get_object_set = \&define_set; our ($all_objects_loaded, $all_change_subscriptions, $all_objects_are_loaded, $all_params_loaded); *all_objects_loaded = \$UR::Context::all_objects_loaded; *all_change_subscriptions = \$UR::Context::all_change_subscriptions; *all_objects_are_loaded = \$UR::Context::all_objects_are_loaded; *all_params_loaded = \$UR::Context::all_params_loaded; # These live in UR::Context, where they may switch to point to # different data structures depending on sub-context, transaction, etc. # They are aliased here for backward compatability, since many parts # of the system use $UR::Object::whatever to work with them directly. sub load { # this is here for backward external compatability # get() now goes directly to the context my $class = shift; if (ref $class) { # Trying to reload a specific object? if (@_) { Carp::confess("load() on an instance with parameters is not supported"); return; } @_ = ('id' ,$class->id()); $class = ref $class; } my ($rule, @extra) = UR::BoolExpr->resolve_normalized($class,@_); if (@extra) { if (scalar @extra == 2 and $extra[0] eq "sql") { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } else { die "Odd parameters passed directly to $class load(): @extra.\n" . "Processable params were: " . Data::Dumper::Dumper({ $rule->params_list }); } } return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } sub _load { Carp::cluck(); my ($class,$rule) = @_; return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } sub dbh { Carp::confess("Attempt to call dbh() on a UR::Object.\n" . "Objects no longer have DB handles, data_sources do\n" . "use resolve_data_sources_for_class_meta_and_rule() on a UR::Context instead"); my $ds = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule(shift->__meta__); return $ds->get_default_dbh; } sub matches { no warnings; my $self = shift; my %param = $self->preprocess_params(@_); for my $key (keys %param) { next unless $self->can($key); return 0 unless $self->$key eq $param{$key} } return 1; } sub property_names { my $class = shift; my $meta = $class->__meta__; return $meta->all_property_names; } sub _is_loaded { Carp::cluck(); my ($class,$rule) = @_; return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); } # as we remove more logic from the default API, add extensions here. use UR::ObjectV04removed; DBI.pm000444023532023421 7042712121654173 13702 0ustar00abrummetgsc000000000000UR-0.41/lib/UR# Additional methods for DBI. package UR::DBI; =pod =head1 NAME UR::DBI - methods for interacting with a database. =head1 SYNOPSIS ##- use UR::DBI; UR::DBI->monitor_sql(1); my $dbh = UR::DBI->connect(...); =head1 DESCRIPTION This module subclasses DBI, and provides a few extra methods useful when using a database. =head1 METHODS =over 4 =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION;; # set up module use base qw(Exporter DBI); our (@EXPORT, @EXPORT_OK); @EXPORT = qw(); @EXPORT_OK = qw(); use IO::Handle; use IO::File; use Time::HiRes; # do not use UR::ModuleBase as base class because it does not play nice with DBI # # UR::DBI control flags # # Build a few class methods to manipulate the environment variables # that control SQL monitoring my %sub_env_map = ( monitor_sql => 'UR_DBI_MONITOR_SQL', monitor_dml => 'UR_DBI_MONITOR_DML', explain_sql_if => 'UR_DBI_EXPLAIN_SQL_IF', explain_sql_slow => 'UR_DBI_EXPLAIN_SQL_SLOW', explain_sql_match => 'UR_DBI_EXPLAIN_SQL_MATCH', explain_sql_callstack => 'UR_DBI_EXPLAIN_SQL_CALLSTACK', no_commit => 'UR_DBI_NO_COMMIT', monitor_every_fetch => 'UR_DBI_MONITOR_EVERY_FETCH', dump_stack_on_connect => 'UR_DBI_DUMP_STACK_ON_CONNECT', ); our ($monitor_sql,$monitor_dml,$no_commit,$monitor_every_fetch,$dump_stack_on_connect, $explain_sql_slow,$explain_sql_if,$explain_sql_match,$explain_sql_callstack); while ( my($subname, $envname) = each ( %sub_env_map ) ) { no strict 'refs'; # There's a scalar of the same name as the sub to hold the value, hook them together *{$subname} = \$ENV{$envname}; my $subref = sub { if (@_ > 1) { $$subname = $_[1]; } return $$subname; }; if ($subname =~ /explain/) { eval "\$$subname = '' if not defined \$$subname"; } else { eval "\$$subname = 0 if not defined \$$subname"; } die $@ if $@; *$subname = $subref; } # by default, monitored SQL goes to STDOUT # FIXME change this 'our' back to a 'my' after we're transisitioned off of the old App API our $sql_fh = IO::Handle->new; $sql_fh->fdopen(fileno(STDERR), 'w'); $sql_fh->autoflush(1); sub sql_fh { $sql_fh = $_[1] if @_ > 1; return $sql_fh; } # # Logging methods # our $log_file; sub log_file { $log_file = pop if @_ > 1; return $log_file; } our $log_fh; my $create_time=0; sub start_logging { return 1 if(defined($log_fh)); return 0 if(-e "$log_file"); $log_fh = new IO::File("> ${log_file}"); unless(defined($log_fh)) { warn "Logging File $log_file Could not be created\n"; return 0; } $create_time=Time::HiRes::time(); return 1; } sub stop_logging { return 1 unless(defined($log_fh)); $log_fh->close; undef $log_fh; } sub log_sql { return 1 unless(defined($log_fh)); my $sql=pop; my $no_timestamp=pop; print $log_fh '=' x 10, "\n" unless($no_timestamp); print $log_fh Time::HiRes::time()-$create_time, "\n" unless($no_timestamp); print $log_fh $sql; } # # Standard DBI overrides # sub connect { my $self = shift; my @params = @_; if ($monitor_sql or $dump_stack_on_connect) { my $time = time; my $time_string = join(' ', $time, '[' . localtime($time) . ']'); $sql_fh->print("DB CONNECT AT: $time_string"); } if ($dump_stack_on_connect) { $sql_fh->print(Carp::longmess()); } $params[2] = 'xxx'; # Param 3 is usually a hashref of connection modifiers if (ref($params[3]) and ref($params[3]) =~ m/HASH/) { my $string = join(', ', map { $_ . ' => ' . $params[3]->{$_} } keys(%{$params[3]}) ); $params[3] = "{ $string }"; } my $params_stringified = join(",", map { defined($_) ? "'$_'" : 'undef' } @params); UR::DBI::before_execute("connecting with params: ($params_stringified)"); my $rv = $self->SUPER::connect(@_); UR::DBI::after_execute(); return $rv; } # # UR::Object hooks # sub commit_all_app_db_objects { my $this_class = shift; my $handle = shift; my $data_source; if ($handle->isa("UR::DBI::db")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle); } elsif ($handle->isa("UR::DBI::st")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle->{Database}); } else { Carp::confess("No handle passed to method!?") } unless ($data_source) { return; } return $data_source->_set_all_objects_saved_committed(); } sub rollback_all_app_db_objects { my $this_class = shift; my $handle = shift; my $data_source; if ($handle->isa("UR::DBI::db")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle); } elsif ($handle->isa("UR::DBI::st")) { $data_source = UR::DataSource::RDBMS->get_for_dbh($handle->{Database}); } else { Carp::confess("No handle passed to method!?") } unless ($data_source) { Carp::confess("No data source found for database handle! $handle") } return $data_source->_set_all_objects_saved_rolled_back(); } my @disable_dump_and_explain; sub _disable_dump_explain { push @disable_dump_and_explain, [$monitor_sql,$explain_sql_slow,$explain_sql_match]; $monitor_sql = 0; $explain_sql_slow = ''; $explain_sql_match = ''; } sub _restore_dump_explain { if (@disable_dump_and_explain) { my $vars = pop @disable_dump_and_explain; ($monitor_sql,$explain_sql_slow,$explain_sql_match) = @$vars; } else { Carp::confess("No state saved for disabled dump/explain"); } } # The before_execute/after_execute subroutine pair # are callbacks called by execute() and by other # methods which implicitly execute a statement. # They use these three varaibles to track state, # presuming that the callback pair cannot be nested. print("\nEXPLAIN QUERY MATCHING /$explain_sql_match/gi" . ($val ne $sql ? " (on value '$val') " : "") ); if ($monitor_sql) { $sql_fh->print("\n"); } else { _print_sql_and_params($sql,@_); } if ($explain_sql_callstack) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } if ($UR::DBI::explained_queries{$sql}) { $sql_fh->print("(query explained above)\n"); } else { UR::DBI::_print_query_plan($sql,$dbh); $UR::DBI::explained_queries{$sql} = 1; } last; } } } my $start_time = _set_start_time(); if ($monitor_sql){ _print_sql_and_params($sql,@_); if ($monitor_sql > 1) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } _print_monitor_label("EXECUTE"); } elsif($monitor_dml && $sql !~ /^\s*select/i){ _print_sql_and_params($sql,@_); _print_monitor_label("EXECUTE"); $monitor_dml=2; } no warnings; UR::DBI::log_sql_for_summary($sql); # $ENV{UR_DBI_SUMMARIZE_SQL} my $log_sql_str = _generate_sql_and_params_log_entry($sql, @_); UR::DBI::log_sql($log_sql_str); return $start_time; } sub after_execute { #my ($sql,@params) = @_; my $elapsed_time = _set_elapsed_time(); if ($monitor_sql){ _print_elapsed_time(); } elsif($monitor_dml == 2){ _print_elapsed_time(); $monitor_dml = 1; } UR::DBI::log_sql(1, ($elapsed_time)."\n"); return $elapsed_time; } # The before_fetch/after_fetch pair are callback # called by fetch() and by other methods which implicitly # fetch data w/o explicitly calling fetch(). our $_fetching = 0; sub before_fetch { my $sth = shift; return if @disable_dump_and_explain; if ($_fetching) { Carp::cluck("before_fetch called after another before_fetch w/o intervening after_fetch!"); } $_fetching = 1; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; if ($monitor_sql) { if ($fetch_timing_arrayref and @$fetch_timing_arrayref == 0) { UR::DBI::_print_monitor_label('FIRST FETCH'); } elsif ($monitor_every_fetch) { UR::DBI::_print_monitor_label('NTH FETCH'); } } return UR::DBI::_set_start_time(); } sub after_fetch { my $sth = shift; return if @disable_dump_and_explain; $_fetching = 0; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; my $time; push @$fetch_timing_arrayref, UR::DBI::_set_elapsed_time(); if ($monitor_sql) { if ($monitor_every_fetch || @$fetch_timing_arrayref == 1) { $time = UR::DBI::_print_elapsed_time(); } } if (@$fetch_timing_arrayref == 1) { my $time = $sth->execute_time + $fetch_timing_arrayref->[0]; UR::DBI::_check_query_timing($sth->{Statement},$time,$sth->{Database},$sth->last_params); } return $time; } sub after_all_fetches_with_sth { my $sth = shift; my $fetch_timing_arrayref = $sth->fetch_timing_arrayref; # This arrayref is set when it goes through the subclass' execute(), # and is removed when we finish all fetches(). # Since a variety of things attempt to call this from the various "final" # positions of an $sth we delete this so the final callback operates only once. # Also, internally generated $sths which do not get executed() normally # will be skipped by this check. if (!$fetch_timing_arrayref) { # internal sth which did not go through prepare() #print $sql_fh "SKIP STH\n"; return; } $sth->fetch_timing_arrayref(undef); my $print_fetch_summary; if ($monitor_sql and $sth->{Statement} =~ /select/i) { $print_fetch_summary = 1; UR::DBI::_print_monitor_label('TOTAL EXECUTE-FETCH'); } my $time = $sth->execute_time; if (@$fetch_timing_arrayref) { for my $fetch_time (@$fetch_timing_arrayref ) { $time += $fetch_time; } if ($print_fetch_summary) { UR::DBI::_print_monitor_time($time); } # since there WERE fetches, we already checked query timing } else { if ($print_fetch_summary) { UR::DBI::_print_monitor_time($time); } # since there were NOT fetches, we check query timing now UR::DBI::_check_query_timing($sth->{Statement},$time,$sth->{Database},$sth->last_params); } return $time; } sub after_all_fetches_no_sth { my ($sql, $time, $dbh, @params) = @_; $time = _set_elapsed_time() unless defined $time; if ($monitor_sql and $sql =~ /select/i) { UR::DBI::_print_monitor_label('TOTAL EXECUTE-FETCH'); UR::DBI::_print_monitor_time($time); } # no sth = no fetches = no query timing check done yet... UR::DBI::_check_query_timing($sql,$time,$dbh,@params); return $time; } my $__SQL_SUMMARY__ = {}; sub log_sql_for_summary { my ($sql) = @_; $__SQL_SUMMARY__->{$sql}++; } sub print_sql_summary { for my $sql (sort {$__SQL_SUMMARY__->{$b} <=> $__SQL_SUMMARY__->{$a}} keys %$__SQL_SUMMARY__) { print STDERR join('',"********************\n", $__SQL_SUMMARY__->{$sql}, " instances of query: $sql\n"); } } # These methods are called by the above. sub _generate_sql_and_params_log_entry { my $sql = shift; no warnings; my $sql_log_str = "\nSQL: $sql\n"; if (@_) { $sql_log_str .= "PARAMS: "; $sql_log_str .= join(", ", map { defined($_) ? "'$_'" : "NULL" } map { scalar(grep { $_ } map { 128 & ord $_ } split(//, substr($_, 0, 64))) ? '' : $_ } @_ ) . "\n"; } return $sql_log_str; } sub _print_sql_and_params { my $sql = shift; my $entry = _generate_sql_and_params_log_entry($sql, @_); no warnings; print $sql_fh $entry; } sub _set_start_time { $start_time=&Time::HiRes::time(); } our $_print_monitor_label_or_time_is_ready_for = "label"; sub _print_monitor_label { #Carp::cluck() unless $_print_monitor_label_or_time_is_ready_for eq "label"; my $time_label = shift; $sql_fh->print("$time_label TIME: "); $_print_monitor_label_or_time_is_ready_for = "time"; } sub _print_monitor_time { #Carp::cluck() unless $_print_monitor_label_or_time_is_ready_for eq "time"; $sql_fh->printf( "%.4f s\n", shift); $_print_monitor_label_or_time_is_ready_for = "label"; } sub _set_elapsed_time { $elapsed_time = &Time::HiRes::time()-$start_time; } sub _print_elapsed_time { _print_monitor_time($elapsed_time); } our $_print_check_for_slow_query = 0; sub _check_query_timing { my ($sql,$time,$dbh,@params) = @_; return if @disable_dump_and_explain; return unless $sql =~ /select/i; print $sql_fh "CHECK FOR SLOW QUERY:\n" if $_print_check_for_slow_query; # used only by a test case if (length($explain_sql_slow) and $time >= $explain_sql_slow) { $sql_fh->print("EXPLAIN QUERY SLOWER THAN $explain_sql_slow seconds ($time):"); if ($monitor_sql || ($monitor_dml && $sql !~ /^\s*select/i)) { $sql_fh->print("\n"); } else { _print_sql_and_params($sql,@params); } if ($explain_sql_callstack) { $sql_fh->print(Carp::longmess("callstack begins"),"\n"); } if ($UR::DBI::explained_queries{$sql}) { $sql_fh->print("(query explained above)\n"); } else { $UR::DBI::explained_queries{$sql} = 1; UR::DBI::_print_query_plan($sql,$dbh); } } } sub _print_query_plan { my ($sql,$dbh,%params) = @_; UR::DBI::_disable_dump_explain(); $dbh->do($UR::DBI::EXPLAIN_PLAN_CLEANUP_DML); # placeholders in explain plan queries on windows # results in Oracle throwing an ORA-00600 error, # likely due to interaction with DBI. Replace with # literals. if ($^O eq "MSWin32" || $^O eq 'cygwin') { $sql =~ s/\?/'1'/g; } $dbh->do($UR::DBI::EXPLAIN_PLAN_DML . "\n" . $sql) or die "Failed to produce query plan! " . $dbh->errstr; UR::DBI::Report->generate( sql => [$UR::DBI::EXPLAIN_PLAN_SQL], dbh => $dbh, count => 0, outfh => $sql_fh, %params, "explain-sql" => 0, "echo" => 0, ); $sql_fh->print("\n"); $dbh->do($UR::DBI::EXPLAIN_PLAN_CLEANUP_DML); UR::DBI::_restore_dump_explain(); return 1; } ############ # # Database handle subclass # ############ package UR::DBI::db; use strict; use warnings; our @ISA = qw(DBI::db); sub commit { my $self = shift; # unless ($no_commit) { # print "\n\n\n************* FORCIBLY SETTING NO-COMMIT FOR TESTING. This would have committeed!!!! **********\n\n\n"; # $no_commit = 1; # } if ($no_commit) { # Respect the ->no_commit(1) setting. UR::DBI::before_execute("commit (ignored)"); UR::DBI::after_execute; return 1; } else { if(UR::DataSource->use_dummy_autogenerated_ids) { # Not cool...you shouldn't have dummy-ids on and no-commit off # Don't commit, and notify the authorities UR::DBI::before_execute("commit (ignored)"); $UR::Context::current->error_message('Tried to commit with dummy-ids on and no-commit off'); UR::DBI::after_execute; #$UR::Context::current->send_email( # To => 'example@example.edu', # Subject => 'attempt to commit with dummy-ids on and no-commit off '. # "by $ENV{USER} on $ENV{HOST} running ". # UR::Context::Process->original_program_path." as pid $$", # Message => "Call stack:\n" .Carp::longmess() #); } else { # Commit and update the associated objects. UR::DBI::before_execute("commit"); my $rv = $self->SUPER::commit(@_); UR::DBI::after_execute; if ($rv) { UR::DBI->commit_all_app_db_objects($self) } return $rv; } } } sub commit_without_object_update { UR::DBI::before_execute("commit (no object updates)"); my $rv = shift->SUPER::commit(@_); UR::DBI::after_execute(); return $rv; } sub rollback { my $self = shift; UR::DBI::before_execute("rollback"); my $rv = $self->SUPER::rollback(@_); UR::DBI::after_execute(); if ($rv) { UR::DBI->rollback_all_app_db_objects($self) } return $rv; } sub rollback_without_object_update { UR::DBI::before_execute("rollback (w/o object updates)"); my $rv = shift->SUPER::commit(@_); UR::DBI::after_execute(); return $rv; } sub disconnect { my $self = shift; # Always rollback. Oracle commits by default on disconnect. $self->rollback; # Msg and disconnect. UR::DBI::before_execute("disconnecting"); my $rv = $self->SUPER::disconnect(@_); UR::DBI::after_execute(); # There doesn't seem to be anything less which # sets this, but legacy tools did if ( (defined $UR::DBI::common_dbh) and ($self eq $UR::DBI::common_dbh) ) { UR::DBI::before_execute("common dbh removed"); $UR::DBI::common_dbh = undef; UR::DBI::after_execute("common dbh removed"); } return $rv; } sub prepare { my $self = shift; my $sql = $_[0]; my $sth; #print $sql_fh "PREPARE: $sql\n"; if ($sql =~ /^\s*(commit|rollback)\s*$/i) { unless ($sql =~ /^(commit|rollback)$/i) { Carp::confess("Executing a statement with an embedded commit/rollback?\n$sql\n"); } if ($sth = $self->SUPER::prepare(@_)) { if ($1 =~ /commit/i) { $UR::DBI::prepared_commit{$sth} = 1; } elsif ($1 =~ /rollback/) { $UR::DBI::prepared_rollback{$sth} = 1; } } } else { $sth = $self->SUPER::prepare(@_) or return; } return $sth; } # For newer versions of DBI, some of the $dbh->select* methods do not # call execute internally, so SQL dumping and logging will not occur. # These are listed below, and the bad ones are overridden. # selectall_hashref ok # selectcol_arrayref ok # selectrow_hashref ok # selectall_arrayref bad # selectrow_arrayref bad # selectrow_array bad sub selectall_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my $ar = $self->SUPER::selectall_arrayref(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectcol_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); UR::DBI::_disable_dump_explain(); my $ar = $self->SUPER::selectcol_arrayref(@_); UR::DBI::_restore_dump_explain(); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectall_hashref { my $self = shift; my @p = ($_[0],@_[3..$#_]); UR::DBI::before_execute($self,@p); UR::DBI::_disable_dump_explain(); my $ar = $self->SUPER::selectall_hashref(@_); UR::DBI::_restore_dump_explain(); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectrow_arrayref { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my $ar = $self->SUPER::selectrow_arrayref(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return $ar; } sub selectrow_array { my $self = shift; my @p = ($_[0],@_[2..$#_]); UR::DBI::before_execute($self,@p); my @a = $self->SUPER::selectrow_array(@_); my $time = UR::DBI::after_execute($self,@p); UR::DBI::after_all_fetches_no_sth($_[0],$time,$self,@p); return @a if wantarray; return $a[0]; } sub DESTROY { UR::DBI::before_execute("destroying connection"); shift->SUPER::DESTROY(@_); UR::DBI::after_execute("destroying connection"); } ######### # # Statement handle subclass # ######### package UR::DBI::st; use strict; use warnings; use Time::HiRes; use Sys::Hostname; use Devel::GlobalDestruction; our @ISA = qw(DBI::st); sub _mk_mutator { my ($class, $method) = @_; # Make a more specific key based on the package # to try not to conflict with anything else. # This must start with 'private_'. See DBI docs on subclassing. my $hash_key = join('_', 'private', lc $class, lc $method); $hash_key =~ s/::/_/g; my $sub = sub { return if Devel::GlobalDestruction::in_global_destruction; my $sth = shift; if (@_) { no warnings 'uninitialized'; $sth->{$hash_key} = shift; } no warnings; return $sth->{$hash_key}; }; no strict; *{$class . '::' . $method} = $sub; } for my $method (qw(execute_time fetch_timing_arrayref last_params_arrayref)) { __PACKAGE__->_mk_mutator($method); } sub last_params { my $ret = shift->last_params_arrayref; unless (defined $ret) { $ret = []; } @{ $ret }; } sub execute { my $sth = shift; # (re)-initialize the timing array if (my $a = $sth->fetch_timing_arrayref()) { # re-executing on a previously used $sth. UR::DBI::after_all_fetches_with_sth($sth); } else { # initialize the $sth on first execute. $sth->fetch_timing_arrayref([]); } $sth->last_params_arrayref([@_]); UR::DBI::before_execute($sth->{Database},$sth->{Statement},@_); my $rv = $sth->SUPER::execute(@_); UR::DBI::after_execute($sth->{Database},$sth->{Statement},@_); # record the elapsed time for execution. $sth->execute_time($UR::DBI::elapsed_time); if ($rv) { if (my $prev = $UR::DBI::prepared_commit{$sth}) { UR::DBI->commit_all_app_db_objects($sth); } if (my $prev = $UR::DBI::prepared_rollback{$sth}) { UR::DBI->rollback_all_app_db_objects($sth); } } return $rv; } sub fetchrow_array { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my @a = $sth->SUPER::fetchrow_array(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return @a if wantarray; return $a[0]; } sub fetchrow_arrayref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchrow_arrayref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return $ar; } sub fetchall_arrayref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchall_arrayref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); UR::DBI::after_all_fetches_with_sth($sth,@_); return $ar; } sub fetchall_hashref { my $sth = shift; my @p = @_[1,$#_]; UR::DBI::before_fetch($sth,@p); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchall_hashref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@p); UR::DBI::after_all_fetches_with_sth($sth,@_[1,$#_]); return $ar; } sub fetchrow_hashref { my $sth = shift; UR::DBI::before_fetch($sth,@_); UR::DBI::_disable_dump_explain(); my $ar = $sth->SUPER::fetchrow_hashref(@_); UR::DBI::_restore_dump_explain(); UR::DBI::after_fetch($sth,@_); return $ar; } sub fetch { my $sth = shift; UR::DBI::before_fetch($sth,@_); my $rv = $sth->SUPER::fetch(@_); UR::DBI::after_fetch($sth,@_); return $rv; } sub finish { my $sth = shift; UR::DBI::after_all_fetches_with_sth($sth); return $sth->SUPER::finish(@_); } sub DESTROY { delete $UR::DBI::prepared_commit{$_[0]}; delete $UR::DBI::prepared_rollback{$_[0]}; #print $sql_fh "DESTROY1\n"; UR::DBI::after_all_fetches_with_sth(@_); # does nothing if called previously by finish() #print $sql_fh "DESTROY2\n"; #Carp::cluck(); shift->SUPER::DESTROY(@_); } $UR::DBI::STATEMENT_ID = $$ . '@' . hostname(); $UR::DBI::EXPLAIN_PLAN_DML = "explain plan set statement_id = '$UR::DBI::STATEMENT_ID' into plan_table for "; $UR::DBI::EXPLAIN_PLAN_SQL = qq/ select LPAD(' ',p.LVL-1) || OPERATION OPERATION, OPTIONS, --(case when p.OBJECT_OWNER is null then '' else p.OBJECT_OWNER || '.' end) -- || p.OBJECT_NAME || (case when p.OBJECT_TYPE is null then '' else ' (' || p.OBJECT_TYPE || ')' end) "OBJECT", (case when i.table_name is not null then i.table_name || '(' || index_column_names || ')' else '' end) "OBJECT_IS_ON", p.COST, p.CARDINALITY CARD, p.BYTES, p.OPTIMIZER, p.CPU_COST CPU, p.IO_COST IO, p.TEMP_SPACE TEMP, i.index_type "index_type", i.last_analyzed "index_analyzed" from ( SELECT plan_table.*, level lvl FROM PLAN_TABLE CONNECT BY prior id = parent_id AND prior statement_id = statement_id START WITH id = 0 AND statement_id = '$UR::DBI::STATEMENT_ID' ) p full join dual on dummy = dummy left join all_indexes i on i.index_name = p.object_name and i.owner = p.object_owner left join ( select index_owner, index_name, LTRIM(MAX(SYS_CONNECT_BY_PATH(ic.column_name,',')) KEEP (DENSE_RANK LAST ORDER BY ic.column_position),',') index_column_names from ( select ic.index_owner, ic.index_name, ic.column_name, ic.column_position from all_ind_columns ic ) ic group by ic.index_owner, ic.index_name connect by index_owner = prior index_owner and index_name = prior index_name and column_position = PRIOR column_position + 1 start with column_position = 1 ) index_columns_stringified on index_columns_stringified.index_owner = i.owner and index_columns_stringified.index_name = i.index_name where p.object_name is not null ORDER BY p.id /; $UR::DBI::EXPLAIN_PLAN_CLEANUP_DML = "delete from plan_table where statement_id = '$UR::DBI::STATEMENT_ID'"; 1; __END__ =pod =back =head1 SEE ALSO UR(3), UR::DataSource::RDBMS(3), UR::Context(3), UR::Object(3) =cut #$Header$ Exit.pm000444023532023421 765212121654173 14175 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Exit; =pod =head1 NAME UR::Exit - methods to allow clean application exits. =head1 SYNOPSIS UR::Exit->exit_handler(\&mysub); UR::Exit->clean_exit($value); =head1 DESCRIPTION This module provides the ability to perform certain operations before an application exits. =cut # set up module require 5.006_000; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; our (@ISA, @EXPORT, @EXPORT_OK); require Exporter; @ISA = qw(Exporter); @EXPORT = qw(); @EXPORT_OK = qw(); use Carp; =pod =head1 METHODS These methods provide exit functionality. =over 4 =item exit_handler UR::Exit->exit_handler(\&mysub); Specifies that a given subroutine be run when the application exits. (Unimplimented!) =cut sub exit_handler { die "Unimplimented"; } =pod =item clean_exit UR::Exit->clean_exit($value); Exit the application, running all registered subroutines. (Unimplimented! Just exits the application directly.) =cut sub clean_exit { my $class = shift; my ($value) = @_; $value = 0 unless defined($value); exit($value); } =pod =item death Catch any die or warn calls. This is a universal place to catch die and warn if debugging. =cut sub death { unless ($ENV{'UR_STACK_DUMP_ON_DIE'}) { return; } # workaround common error if ($_[0] =~ /Can.*t upgrade that kind of scalar during global destruction/) { exit 1; } if (defined $^S) { # $^S is defined when perl is executing (as opposed to interpreting) if ($^S) { # $^S is true when its executing in an eval, false outside of one return; } } else { # interpreter is parsing a module or string eval # check the call stack depth for up-stream evals # fall back to perls default handler if there is one my $call_stack_depth = 0; for (1) { my @details = caller($call_stack_depth); #print Data::Dumper::Dumper(\@details); last if scalar(@details) == 0; if ($details[1] =~ /\(eval .*\)/) { #print ""; return; } elsif ($details[3] eq "(eval)") { #print ""; return; } $call_stack_depth++; redo; } } if ( $_[0] =~ /\n$/ and UNIVERSAL::can("UR::Context::Process","is_initialized") and defined(UR::Context::Process->is_initialized) and (UR::Context::Process->is_initialized == 1) ) { # Do normal death if there is a newline at the end, and all other # things are sane. return; } else { # Dump the call stack in other cases. # This is a developer error occurring while things are # initializing. local $Carp::CarpLevel = 1; Carp::confess(@_); return; } } =pod =item warning Give more informative warnings. =cut sub warning { unless ($ENV{'UR_STACK_DUMP_ON_WARN'}) { warn @_; return; } return if $_[0] =~ /Attempt to free unreferenced scalar/; return if $_[0] =~ /Use of uninitialized value in exit at/; return if $_[0] =~ /Use of uninitialized value in subroutine entry at/; return if $_[0] =~ /One or more DATA sections were not processed by Inline/; UR::ModuleBase->warning_message(@_); if ($_[0] =~ /Deep recursion on subroutine/) { print STDERR "Forced exit by UR::Exit on deep recursion.\n"; print STDERR Carp::longmess("Stack tail:"); exit 1; } return; } #$SIG{__DIE__} = \&death unless ($SIG{__DIE__}); #$SIG{__WARN__} = \&warning unless ($SIG{__WARN__}); sub enable_hooks_for_warn_and_die { $SIG{__DIE__} = \&death; $SIG{__WARN__} = \&warning; } &enable_hooks_for_warn_and_die(); 1; __END__ =pod =back =head1 SEE ALSO UR(3), Carp(3) =cut #$Header$ Vocabulary.pm000444023532023421 375612121654173 15374 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::Vocabulary; use strict; use warnings; use Lingua::EN::Inflect ("PL_V","PL"); require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Vocabulary', is => ['UR::Singleton'], doc => 'A word in the vocabulary of a given namespace.', ); sub get_words_with_special_case { shift->_singleton_class_name->_words_with_special_case; } sub _words_with_special_case { return ('UR'); } sub convert_to_title_case { my $conversion_hashref = shift->_words_with_special_case_hashref; my @results; for my $word_in(@_) { my $word = lc($word_in); if (my $uc = $conversion_hashref->{$word}) { push @results, $uc; } else { push @results, ucfirst($word); } } return $results[0] if @results == 1 and !wantarray; return @results; } sub convert_to_special_case { my $conversion_hashref = shift->_words_with_special_case_hashref; my @results; for my $word_in(@_) { my $word = lc($word_in); if (my $sc = $conversion_hashref->{$word}) { push @results, $sc; } else { push @results, $word_in; } } return $results[0] if @results == 1 and !wantarray; return @results; } sub _words_with_special_case_hashref { my $self = shift->_singleton_object; my $hashref = $self->{_words_with_special_case_hashref}; return $hashref if $hashref; $hashref = { map { lc($_) => $_ } $self->get_words_with_special_case }; $self->{_words_with_special_case_hashref} = $hashref; return $hashref; } sub singular_to_plural { my $self = shift; return map { PL($_) } @_; } our %exceptions = ( statuses => 'status', is => 'is', has => 'has', cds => 'cds', ); sub plural_to_singular { my $self = shift; my ($lc,$override); return map { $lc = lc($_); $override = $exceptions{$lc}; ( $override ? $override : PL_V($_) ) } @_; } 1; ObjectDeprecated.pm000444023532023421 3335712121654173 16474 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Object; # deprecated parts of the UR::Object API use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION; use Data::Dumper; use Scalar::Util qw(blessed); sub get_with_special_parameters { # When overridden, this allows a class to take non-properties as parameters # to get(), and handle loading in a special way. Ideally this is handled by # a custom data source, or properties with smart definitions. my $class = shift; my $rule = shift; Carp::confess( "Unknown parameters to $class get(). " . "Implement get_with_special_parameters() to handle non-standard" . " (non-property) query options.\n" . "The special params were " . Dumper(\@_) . "Rule ID: " . $rule->id . "\n" ); } sub get_or_create { my $self = shift; return $self->get( @_ ) || $self->create( @_ ); } sub set { my $self = shift; my @rvals; while (@_) { my $property_name = shift; my $value = shift; push(@rvals, $self->$property_name($value)); } if(wantarray) { return @rvals; } else { return \@rvals; } } sub property_diff { # Ret hashref of the differences between the object and some other object. # The "other object" may be a hashref or hash, in which case it will # treat each key as a property. my ($self, $other) = @_; my $diff = {}; # If we got a hash instead of a hashref... if (@_ > 2) { shift; $other = { @_ } } no warnings; my $self_value; my $other_value; my $class_object = $self->__meta__; for my $property ($class_object->all_property_names) { if (ref($other) eq 'HASH') { next unless exists $other->{$property}; $other_value = $other->{$property}; } else { $other_value = $other->$property; } $self_value = $self->$property; $diff->{$property} = $self_value if ($other_value ne $self_value); } return $diff; } # TODO: make this a context operation sub unload { my $proto = shift; my ($self, $class); ref $proto ? $self = $proto : $class = $proto; my $cx = $UR::Context::current; if ( $self ) { # object method # The only things which can be unloaded are things committed to # their database in the exact same state. Everything else must # be reverted or deleted. return unless $self->{db_committed}; if ($self->__changes__) { #warn "NOT UNLOADING CHANGED OBJECT! $self $self->{id}\n"; return; } $self->__signal_change__('unload'); if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM UNLOAD object $self class ",$self->class," id ",$self->id,"\n"; } $cx->_abandon_object($self); return $self; } else { # class method # unload the objects in the class # where there are subclasses of the class # delegate to them my @unloaded; # unload all objects of this class my @involved_classes = ( $class ); for my $obj ($cx->all_objects_loaded_unsubclassed($class)) { push @unloaded, $obj->unload; } # unload any objects that belong to any subclasses for my $subclass ($cx->__meta__->subclasses_loaded($class)) { push @involved_classes, $subclass; push @unloaded, $subclass->unload; } # get rid of the loading info matching this class foreach my $template_id ( keys %$UR::Context::all_params_loaded ) { if (UR::BoolExpr::Template->get($template_id)->subject_class_name->isa($class)) { delete $UR::Context::all_params_loaded->{$template_id}; } } # Turn off the all_objects_are_loaded flags delete @$UR::Context::all_objects_are_loaded{@involved_classes}; return @unloaded; } } # TODO: replace internal calls to go right to the context method sub is_loaded { # this is just here for backward compatability for external calls # get() now goes to the context for data # This shortcut handles the most common case rapidly. # A single ID is passed-in, and the class name used is # not a super class of the specified object. # This logic is in both get() and is_loaded(). my $quit_early = 0; if ( @_ == 2 && !ref($_[1]) ) { unless (defined($_[1])) { Carp::confess(); } my $obj = $UR::Context::all_objects_loaded->{$_[0]}->{$_[1]}; return $obj if $obj; # we could safely return nothing right now, except # that a subclass of this type may have the object return unless $_[0]->__meta__->subclasses_loaded; # nope, there were no subclasses } my $class = shift; my $rule = UR::BoolExpr->resolve_normalized($class,@_); return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); } sub subclasses_loaded { return shift->__meta__->subclasses_loaded(); } # THESE SHOULD PROBABLY GO ON THE CLASS META sub all_objects_are_loaded { # Keep track of which classes claim that they are completely loaded, and that no more loading should be done. # Classes which have the above function return true should set this after actually loading everything. # This class will do just that if it has to load everything itself. my $class = shift; #$meta = $class->__meta__; if (@_) { # Setting the attribute $UR::Context::all_objects_are_loaded->{$class} = shift; } elsif (! exists $UR::Context::all_objects_are_loaded->{$class}) { # unknown... ask the parent classes and remember the answer foreach my $parent_class ( $class->inheritance ) { if (exists $UR::Context::all_objects_are_loaded->{$parent_class}) { $UR::Context::all_objects_are_loaded->{$class} = $UR::Context::all_objects_are_loaded->{$parent_class}; last; } } } return $UR::Context::all_objects_are_loaded->{$class}; } # Observer pattern (old) sub create_subscription { my $self = shift; my %params = @_; # parse parameters my ($class,$id,$method,$callback,$note,$priority); my %observer_params; @observer_params{'aspect','callback','note','priority','subject_id'} = delete @params{'method','callback','note','priority','id'}; $observer_params{'subject_class_name'} = $self->class; $observer_params{'priority'} = 1 unless defined $observer_params{'priority'}; if (!defined $observer_params{'subject_id'} and ref($self)) { $observer_params{'subject_id'} = $self->id; } if (my @unknown = keys %params) { Carp::croak "Unknown options @unknown passed to create_subscription!"; } # validate if (my @bad_params = %params) { Carp::croak "Bad params passed to add_listener: @bad_params"; } my $observer = UR::Observer->create(%observer_params); return unless $observer; return [@observer_params{'subject_class_name','subject_id','aspect','callback','note'}]; } sub validate_subscription { return 1; my ($self,$subscription_property) = @_; Carp::confess("The _create_object and _delete_object signals are no longer emitted!") if defined($subscription_property) and ($subscription_property eq '_create_object' or $subscription_property eq '_delete_object'); # Undefined attributes indicate that the subscriber wants any changes at all to generate a callback. return 1 if (!defined($subscription_property)); # All standard creation and destruction methods emit a signal. return 1 if ($subscription_property =~ /^(create|delete|commit|rollback|load|unload|load_external)$/); # A defined attribute in our property list indicates the caller wants callbacks from our properties. my $class_object = $self->__meta__; for my $property ($class_object->all_property_names) { return 1 if $property eq $subscription_property; } return 1 if ($class_object->_is_valid_signal($subscription_property)); # Bad subscription request. return; } sub inform_subscription_cancellation { # This can be overridden in derived classes if the class wants to know # when subscriptions are cancelled. return 1; } sub cancel_change_subscription ($@) { my ($class,$id,$property,$callback,$note); if (@_ >= 4) { ($class,$id,$property,$callback,$note) = @_; die "Bad parameters." if ref($class); } elsif ( (@_==3) or (@_==2) ) { ($class, $property, $callback) = @_; if (ref($_[0])) { $class = ref($_[0]); $id = $_[0]->id; } } else { die "Bad parameters."; } my %params; if (defined $class) { $params{'subject_class_name'} = $class; } if (defined $id) { $params{'subject_id'} = $id; } if (defined $property) { $params{'aspect'} = $property; } if (defined $callback) { $params{'callback'} = $callback; } if (defined $note) { $params{'note'} = $note; } my @observers = UR::Observer->get(%params); return unless @observers; if (@observers > 1) { Carp::croak('Matched more than one observer within cancel_change_subscription(). Params were: ' . join(', ', map { "$_ => " . $params{$_} } keys %params)); } $observers[0]->delete(); } # This should go away when we shift to fully to a transaction log for deletions. sub ghost_class { my $class = $_[0]->class; $class = $class . '::Ghost'; return $class; } package UR::ModuleBase; # Method for setting a callback using the old, non-command messaging API =pod =over =item message_callback $sub_ref = UR::ModuleBase->message_callback($type); UR::ModuleBase->message_callback($type, $sub_ref); This method returns and optionally sets the subroutine that handles messages of a specific type. =cut ## set or return a callback that has been created for a message type sub message_callback { my $self = shift; my ($type, $callback) = @_; my $methodname = $type . '_messages_callback'; if (!$callback) { # to clear the old, deprecated non-command messaging API callback return UR::Object->$methodname($callback); } my $wrapper_callback = sub { my($obj,$msg) = @_; my $obj_class = $obj->class; my $obj_id = (ref($obj) ? ($obj->can("id") ? $obj->id : $obj) : $obj); my $message_package = $type . '_package'; my $message_object = UR::ModuleBase::Message->create ( text => $msg, level => 1, package_name => $obj->$message_package(), call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); $callback->($message_object, $obj, $type); $_[1] = $message_object->text; }; # To support the old, deprecated, non-command messaging API UR::Object->$methodname($wrapper_callback); } sub message_object { my $self = shift; # see how we were called if (@_ < 2) { no strict 'refs'; # return the message object my ($type) = @_; my $method = $type . '_message'; my $msg_text = $self->method(); my $obj_class = $self->class; my $obj_id = (ref($self) ? ($self->can("id") ? $self->id : $self) : $self); my $msgdata = $self->_get_msgdata(); return UR::ModuleBase::Message->create ( text => $msg_text, level => 1, package_name => $msgdata->{$type . '_package'}, call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); } } foreach my $type ( UR::ModuleBase->message_types ) { my $retriever_name = $type . '_text'; my $compat_name = $type . '_message'; my $sub = sub { my $self = shift; return $self->$compat_name(); }; no strict 'refs'; *$retriever_name = $sub; } # class that stores and manages messages for the deprecated API package UR::ModuleBase::Message; use Scalar::Util qw(weaken); ##- use UR::Util; UR::Util->generate_readonly_methods ( text => undef, level => undef, package_name => undef, call_stack => [], time_stamp => undef, owner_class => undef, owner_id => undef, type => undef, ); sub create { my $class = shift; my $obj = {@_}; bless ($obj,$class); weaken $obj->{'owner_id'} if (ref($obj->{'owner_id'})); return $obj; } sub owner { my $self = shift; my ($owner_class,$owner_id) = ($self->owner_class, $self->owner_id); if (not defined($owner_id)) { return $owner_class; } elsif (ref($owner_id)) { return $owner_id; } else { return $owner_class->get($owner_id); } } sub string { my $self = shift; "$self->{time_stamp} $self->{type}: $self->{text}\n"; } sub _stack_item_params { my ($self, $stack_item) = @_; my ($function, $parameters, @parameters); return unless ($stack_item =~ s/\) called at [^\)]+ line [^\)]+\s*$/\)/); if ($stack_item =~ /^\s*([^\(]*)(.*)$/) { $function = $1; $parameters = $2; @parameters = eval $parameters; return ($function, @parameters); } else { return; } } package UR::Object; 1; Value.pm000444023532023421 340312121654173 14326 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Value; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; our @CARP_NOT = qw( UR::Context ); UR::Object::Type->define( class_name => 'UR::Value', is => 'UR::Object', has => ['id'], data_source => 'UR::DataSource::Default', ); sub __display_name__ { return shift->id; } sub __load__ { my $class = shift; my $rule = shift; my $expected_headers = shift; my $id = $rule->value_for_id; unless (defined $id) { #$DB::single = 1; Carp::croak "Can't load an infinite set of $class. Some id properties were not specified in the rule $rule"; } if (ref($id) and ref($id) eq 'ARRAY') { # We're being asked to load up more than one object. In the basic case, this is only # possible if the rule _only_ contains ID properties. For anything more complicated, # the subclass should implement its own behavior my $class_meta = $class->__meta__; my %id_properties = map { $_ => 1 } $class_meta->all_id_property_names; my @non_id = grep { ! $id_properties{$_} } $rule->template->_property_names; if (@non_id) { Carp::croak("Cannot load class $class via UR::DataSource::Default when 'id' is a listref and non-id properties appear in the rule:" . join(', ', @non_id)); } my $count = @$expected_headers; my $listifier = sub { my $c = $count; my @l; push(@l,$_[0]) while ($c--); return \@l }; return ($expected_headers, [ map { &$listifier($_) } @$id ]); } my @values; foreach my $header ( @$expected_headers ) { my $value = $rule->value_for($header); push @values, $value; } return $expected_headers, [\@values]; } sub underlying_data_types { return (); } 1; Manual.pod000444023532023421 160112121654173 14633 0ustar00abrummetgsc000000000000UR-0.41/lib/UR=pod =head1 NAME UR::Manual - Short list of UR's documentation =head1 Manuals L - Short introduction L - UR from Ten Thousand Feet L - Getting started with UR L - A few things to keep in mind when designing a database schema L - Slides for a presentation on UR L - Recepies for getting stuff working L - UR's metadata system L - Defining classes L - UR's command line tool =head1 Basic Entities L - Pretty much everything is-a UR::Object L - Metadata class for Classes L - Metadata class for Properties L - Manage packages and classes L - Software transactions and More! L - How and where to get data DataSource.pm000444023532023421 6626712121654173 15345 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::DataSource; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use Sys::Hostname; *namespace = \&get_namespace; UR::Object::Type->define( class_name => 'UR::DataSource', is_abstract => 1, doc => 'A logical database, independent of prod/dev/testing considerations or login details.', has => [ namespace => { calculate_from => ['id'] }, is_connected => { is => 'Boolean', default_value => 0, is_optional => 1, is_transient => 1 }, ], ); our @CARP_NOT = qw(UR::Context UR::DataSource::QueryPlan); sub define { shift->__define__(@_) } sub get_namespace { my $class = shift->class; return substr($class,0,index($class,"::DataSource")); } sub get_name { my $class = shift->class; return lc(substr($class,index($class,"::DataSource")+14)); } # The default used to be to force table/column/constraint/etc names to # upper case when storing them in the MetaDB, and in the column_name # metadata for properties. The new behavior is to just use whatever the # database supplies us when interrogating the data dictionary. # For datasources/clases that still need the old behavior, override this # to make the column_name metadata for properties forced to upper-case sub table_and_column_names_are_upper_case { 0; } # Basic, dumb data sources do not support joins within a single # query. Instead the Context logic can perform a cross datasource # join within irs own code sub does_support_joins { 0; } # Most datasources do not support recursive queries # Oracle and Postgres do, but in different ways # For data sources without support, it'll have to do multiple queries # to get all the data sub does_support_recursive_queries { ''; } our $use_dummy_autogenerated_ids; *use_dummy_autogenerated_ids = \$ENV{UR_USE_DUMMY_AUTOGENERATED_IDS}; sub use_dummy_autogenerated_ids { # This allows the saved SQL from sync database to be comparable across executions. # It also my $class = shift; if (@_) { ($use_dummy_autogenerated_ids) = @_; } $use_dummy_autogenerated_ids ||= 0; # Replace undef with 0 return $use_dummy_autogenerated_ids; } our $last_dummy_autogenerated_id; sub next_dummy_autogenerated_id { unless($last_dummy_autogenerated_id) { my $hostname = hostname(); $hostname =~ /(\d+)/; my $id = $1 ? $1 : 1; $last_dummy_autogenerated_id = ($id * -10_000_000) - ($$ * 1_000); } #limit id to fit within 11 characters ($last_dummy_autogenerated_id) = $last_dummy_autogenerated_id =~ m/(-\d{1,10})/; return --$last_dummy_autogenerated_id; } sub autogenerate_new_object_id_for_class_name_and_rule { my $ds = shift; if (ref $ds) { $ds = ref($ds) . " ID " . $ds->id; } # Maybe we could use next_dummy_autogenerated_id instead? die "Data source $ds did not implement autogenerate_new_object_id_for_class_name_and_rule()"; } # UR::Context needs to know if a data source supports savepoints sub can_savepoint { my $class = ref($_[0]); die "Class $class didn't supply can_savepoint()"; } sub set_savepoint { my $class = ref($_[0]); die "Class $class didn't supply set_savepoint, but can_savepoint is true"; } sub rollback_to_savepoint { my $class = ref($_[0]); die "Class $class didn't supply rollback_to_savepoint, but can_savepoint is true"; } sub _get_class_data_for_loading { my ($self, $class_meta) = @_; my $class_data = $class_meta->{loading_data_cache}; unless ($class_data) { $class_data = $self->_generate_class_data_for_loading($class_meta); } return $class_data; } sub _resolve_query_plan { my ($self, $rule_template) = @_; my $qp = UR::DataSource::QueryPlan->get( rule_template => $rule_template, data_source => $self, ); $qp->_init() unless $qp->_is_initialized; return $qp; } # Child classes can override this to return a different datasource # depending on the rule passed in sub resolve_data_sources_for_rule { return $_[0]; } sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $class_name = $class_meta->class_name; my $ghost_class = $class_name->ghost_class; my @all_id_property_names = $class_meta->all_id_property_names(); my @id_properties = $class_meta->id_property_names; my $id_property_sorter = $class_meta->id_property_sorter; my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my @parent_class_objects = $class_meta->ancestry_class_metas; my $sub_classification_method_name; my ($sub_classification_meta_class_name, $subclassify_by); my @all_properties; my $first_table_name; for my $co ( $class_meta, @parent_class_objects ) { my $table_name = $co->table_name || '__default__'; $first_table_name ||= $table_name; $sub_classification_method_name ||= $co->sub_classification_method_name; $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_properties, map { [$co, $_, $table_name, 0]} sort $sort_sub UR::Object::Property->get(class_name => $co->class_name); } my $sub_typing_property = $class_meta->subclassify_by; my $class_table_name = $class_meta->table_name; my $class_data = { class_name => $class_name, ghost_class => $class_name->ghost_class, parent_class_objects => [$class_meta->ancestry_class_metas], ## sub_classification_method_name => $sub_classification_method_name, sub_classification_meta_class_name => $sub_classification_meta_class_name, subclassify_by => $subclassify_by, all_properties => \@all_properties, all_id_property_names => [$class_meta->all_id_property_names()], id_properties => [$class_meta->id_property_names], id_property_sorter => $class_meta->id_property_sorter, sub_typing_property => $sub_typing_property, # these seem like they go in the RDBMS subclass, but for now the # "table" concept is stretched to mean any valid structure identifier # within the datasource. first_table_name => $first_table_name, class_table_name => $class_table_name, }; return $class_data; } sub _generate_loading_templates_arrayref { # Each entry represents a table alias in the query. # This accounts for different tables, or multiple occurrances # of the same table in a join, by grouping by alias instead of # table. my $class = shift; my $db_cols = shift; my $obj_joins = shift; my $bxt = shift; use strict; use warnings; my %obj_joins_by_source_alias; if (0) { # ($obj_joins) { my @obj_joins = @$obj_joins; while (@obj_joins) { my $foreign_alias = shift @obj_joins; my $data = shift @obj_joins; for my $foreign_property_name (sort keys %$data) { next if $foreign_property_name eq '-is_required'; my $source_alias = $data->{$foreign_property_name}{'link_alias'}; my $detail = $obj_joins_by_source_alias{$source_alias}{$foreign_alias} ||= {}; # warnings come from the above because we don't have 'link_alias' in filters. my $source_property_name = $data->{$foreign_property_name}{'link_property_name'}; if ($source_property_name) { # join my $links = $detail->{links} ||= []; push @$links, $foreign_property_name, $source_property_name; } if (exists $data->{value}) { # filter my $operator = $data->{operator}; my $value = $data->{value}; my $filter = $detail->{filter} ||= []; my $key = $foreign_property_name; $key .= ' ' . $operator if $operator; push @$filter, $key, $value; } } } } else { #Carp::cluck("no obj joins???"); } my %templates; my $pos = 0; my @templates; my %alias_object_num; for my $col_data (@$db_cols) { my ($class_obj, $prop, $table_alias, $object_num, $class_name) = @$col_data; unless (defined $object_num) { die "No object num for loading template data?!"; } #Carp::confess() unless $table_alias; my $template = $templates[$object_num]; unless ($template) { $template = { object_num => $object_num, table_alias => $table_alias, data_class_name => $class_obj->class_name, final_class_name => $class_name || $class_obj->class_name, property_names => [], column_positions => [], id_property_names => undef, id_column_positions => [], id_resolver => undef, # subref }; $templates[$object_num] = $template; $alias_object_num{$table_alias} = $object_num; } push @{ $template->{property_names} }, $prop->property_name; push @{ $template->{column_positions} }, $pos; $pos++; } # Post-process the template objects a bit to get the exact id positions. for my $template (@templates) { next unless $template; # This join may have resulted in no template?! my @id_property_names; for my $id_class_name ($template->{data_class_name}, $template->{data_class_name}->inheritance) { my $id_class_obj = UR::Object::Type->get(class_name => $id_class_name); last if @id_property_names = $id_class_obj->id_property_names; } $template->{id_property_names} = \@id_property_names; my @id_column_positions; for my $id_property_name (@id_property_names) { for my $n (0..$#{ $template->{property_names} }) { if ($template->{property_names}[$n] eq $id_property_name) { push @id_column_positions, $template->{column_positions}[$n]; last; } } } $template->{id_column_positions} = \@id_column_positions; if (@id_column_positions == 1) { $template->{id_resolver} = sub { return $_[0][$id_column_positions[0]]; } } elsif (@id_column_positions > 1) { my $class_name = $template->{data_class_name}; $template->{id_resolver} = sub { my $self = shift; return $class_name->__meta__->resolve_composite_id_from_ordered_values(@$self[@id_column_positions]); } } else { Carp::croak("Can't determine which columns will hold the ID property data for class " . $template->{data_class_name} . ". It's ID properties are (" . join(', ', @id_property_names) . ") which do not appear in the class' property list (" . join(', ', @{$template->{'property_names'}}).")"); } my $source_alias = $template->{table_alias}; if (0 and my $join_data_for_source_table = $obj_joins_by_source_alias{$source_alias}) { # there are joins which come from this entity to other entities # as these entities are loaded, remember the individual queries covered by this object returning # NOTE: when we join a <> b, we remember that we've loaded all of the b for a when _a_ loads, not b, # since it's possible that there ar zero of b, and we don't want to perform the query for b my $source_object_num = $template->{object_num}; my $source_class_name = $template->{data_class_name}; my $next_joins = $template->{next_joins} ||= []; for my $foreign_alias (keys %$join_data_for_source_table) { my $foreign_object_num = $alias_object_num{$foreign_alias}; Carp::confess("no alias for $foreign_alias?") if not defined $foreign_object_num; my $foreign_template = $templates[$foreign_object_num]; my $foreign_class_name = $foreign_template->{data_class_name}; my $join_data = $join_data_for_source_table->{$foreign_alias}; my %links = map { $_ ? @$_ : () } $join_data->{links}; my %filters = map { $_ ? @$_ : () } $join_data->{filters}; my @keys = sort (keys %links, keys %filters); my @value_position_source_property; for (my $n = 0; $n < @keys; $n++) { my $key = $keys[$n]; if ($links{$key} and $filters{$key}) { Carp::confess("unexpected same key $key in filters and joins"); } my $source_property_name = $links{$key}; next unless $source_property_name; push @value_position_source_property, $n, $source_property_name; } my $bx = $foreign_class_name->define_boolexpr(map { $_ => $filters{$_} } @keys); my ($bxt, @values) = $bx->template_and_values(); push @$next_joins, [ $bxt->id, \@values, \@value_position_source_property ]; } } } return \@templates; } sub create_iterator_closure_for_rule_template_and_values { my ($self, $rule_template, @values) = @_; my $rule = $rule_template->get_rule_for_values(@values); return $self->create_iterator_closure_for_rule($rule); } sub _reclassify_object_loading_info_for_new_class { my $self = shift; my $loading_info = shift; my $new_class = shift; my $new_info; %$new_info = %$loading_info; foreach my $template_id (keys %$loading_info) { my $target_class_rules = $loading_info->{$template_id}; foreach my $rule_id (keys %$target_class_rules) { my $pos = index($rule_id,'/'); $new_info->{$template_id}->{$new_class . "/" . substr($rule_id,$pos+1)} = 1; } } return $new_info; } sub _get_object_loading_info { my $self = shift; my $obj = shift; my %param_load_hash; if ($obj->{'__load'}) { while( my($template_id, $rules) = each %{ $obj->{'__load'} } ) { foreach my $rule_id ( keys %$rules ) { $param_load_hash{$template_id}->{$rule_id} = $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; } } } return \%param_load_hash; } sub _add_object_loading_info { my $self = shift; my $obj = shift; my $param_load_hash = shift; while( my($template_id, $rules) = each %$param_load_hash) { foreach my $rule_id ( keys %$rules ) { $obj->{'__load'}->{$template_id}->{$rule_id} = $rules->{$rule_id}; } } } # same as add_object_loading_info, but manipulates the data in $UR::Context::all_params_loaded sub _record_that_loading_has_occurred { my $self = shift; my $param_load_hash = shift; while( my($template_id, $rules) = each %$param_load_hash) { foreach my $rule_id ( keys %$rules ) { $UR::Context::all_params_loaded->{$template_id}->{$rule_id} ||= $rules->{$rule_id}; } } } sub _first_class_in_inheritance_with_a_table { # This is called once per subclass and cached in the subclass from then on. my $self = shift; my $class = shift; $class = ref($class) if ref($class); unless ($class) { Carp::confess("No class?"); } my $class_object = $class->__meta__; my $found = ""; for ($class_object, $class_object->ancestry_class_metas) { if ($_->table_name) { $found = $_->class_name; last; } } #eval qq/ # package $class; # sub _first_class_in_inheritance_with_a_table { # return '$found' if \$_[0] eq '$class'; # shift->SUPER::_first_class_in_inheritance_with_a_table(\@_); # } #/; #die "Error setting data in subclass: $@" if $@; return $found; } sub _class_is_safe_to_rebless_from_parent_class { my ($self, $class, $was_loaded_as_this_parent_class) = @_; my $fcwt = $self->_first_class_in_inheritance_with_a_table($class); unless ($fcwt) { Carp::croak("Can't call _class_is_safe_to_rebless_from_parent_class(): Class $class has no parent classes with a table"); } return ($was_loaded_as_this_parent_class->isa($fcwt)); } sub _CopyToAlternateDB { # This is used to copy data loaded from the primary database into # a secondary database. One use is for setting up an alternate DB # for testing by priming it from data from the "live" DB # # This is called from inside load() when the env var UR_TEST_FILLDB # is set. For now, this alternate DB is always an SQLIte DB, and the # value of the env var is the base name of the file used as its storage. my($self,$load_class_name,$orig_dbh,$data) = @_; our %ALTERNATE_DB; my $dbname = $orig_dbh->{'Name'}; my $dbh; if ($ALTERNATE_DB{$dbname}->{'dbh'}) { $dbh = $ALTERNATE_DB{$dbname}->{'dbh'}; } else { my $filename = sprintf("%s.%s.sqlite", $ENV{'UR_TEST_FILLDB'}, $dbname); # FIXME - The right way to do this is to create a new UR::DataSource::SQLite object instead of making a DBI object directly unless ($dbh = $ALTERNATE_DB{$dbname}->{'dbh'} = DBI->connect("dbi:SQLite:dbname=$filename","","")) { $self->error_message("_CopyToAlternateDB: Can't DBI::connect() for filename $filename" . $DBI::errstr); return; } $dbh->{'AutoCommit'} = 0; } # Find out what tables this query will require my @isa = ($load_class_name); my(%tables,%class_tables); while (@isa) { my $class = shift @isa; next if $class_tables{$class}; my $class_obj = $class->__meta__; next unless $class_obj; my $table_name = $class_obj->table_name; next unless $table_name; $class_tables{$class} = $table_name; foreach my $col ( $class_obj->direct_column_names ) { # FIXME Why are some of the returned column_names undef? next unless defined($col); # && defined($data->{$col}); $tables{$table_name}->{$col} = $data->{$col} } { no strict 'refs'; my @parents = @{$class . '::ISA'}; push @isa, @parents; } } # For each parent class with a table, tell it to create itself foreach my $class ( keys %class_tables ) { next if (! $class_tables{$class} || $ALTERNATE_DB{$dbname}->{'tables'}->{$class_tables{$class}}++); my $class_obj = $class->__meta__(); $class_obj->mk_table($dbh); #unless ($class_obj->mk_table($dbh)) { # $dbh->rollback(); # return undef; #} } # Insert the data into the alternate DB foreach my $table_name ( keys %tables ) { my $sql = "INSERT INTO $table_name "; my $num_values = (values %{$tables{$table_name}}); $sql .= "(" . join(',',keys %{$tables{$table_name}}) . ") VALUES (" . join(',', map {'?'} (1 .. $num_values)) . ")"; my $sth = $dbh->prepare_cached($sql); unless ($sth) { $self->error_message("Error in prepare to alternate DB: $DBI::errstr\nSQL: $sql"); $dbh->rollback(); return undef; } unless ( $sth->execute(values %{$tables{$table_name}}) ) { $self->warning_message("Can't insert into $table_name in alternate DB: ".$DBI::errstr."\nSQL: $sql\nPARAMS: ". join(',',values %{$tables{$table_name}})); # We might just be inserting data that's already there... # This is the error message sqlite returns if ($DBI::errstr !~ m/column (\w+) is not unique/i) { $dbh->rollback(); return undef; } } } $dbh->commit(); 1; } sub _get_current_entities { my $self = shift; my @class_meta = UR::Object::Type->is_loaded( data_source_id => $self->id ); my @objects; for my $class_meta (@class_meta) { next unless $class_meta->generated(); # Ungenerated classes won't have any instances my $class_name = $class_meta->class_name; push @objects, $UR::Context::current->all_objects_loaded($class_name); } return @objects; } sub _prepare_for_lob { }; sub _set_specified_objects_saved_uncommitted { my ($self,$objects_arrayref) = @_; # Sets an objects as though the has been saved but tha changes have not been committed. # This is called automatically by _sync_databases. my %objects_by_class; my $class_name; for my $object (@$objects_arrayref) { $class_name = ref($object); $objects_by_class{$class_name} ||= []; push @{ $objects_by_class{$class_name} }, $object; } for my $class_name (sort keys %objects_by_class) { my $class_object = $class_name->__meta__; my @property_names = map { $_->property_name } grep { $_->column_name } $class_object->all_property_metas; for my $object (@{ $objects_by_class{$class_name} }) { $object->{db_saved_uncommitted} ||= {}; my $db_saved_uncommitted = $object->{db_saved_uncommitted}; for my $property ( @property_names ) { $db_saved_uncommitted->{$property} = $object->$property; } } } return 1; } sub _set_all_objects_saved_committed { # called by UR::DBI on commit my $self = shift; return $self->_set_specified_objects_saved_committed([ $self->_get_current_entities ]); } sub _set_all_specified_objects_saved_committed { my $self = shift; my($pkg, $file, $line) = caller; Carp::carp("Deprecated method _set_all_specified_objects_saved_committed called at file $file line $line. The new name for this method is _set_specified_objects_saved_committed"); my @changed_objects = @_; $self->_set_specified_objects_saved_committed(\@changed_objects); } sub _set_specified_objects_saved_committed { my $self = shift; my $objects = shift; # Two step process... set saved and committed, then fire commit observers. # Doing so prevents problems should any of the observers themselves commit. my @saved_objects; for my $obj (@$objects) { my $saved = $self->_set_object_saved_committed($obj); push @saved_objects, $saved if $saved; } for my $obj (@saved_objects) { next if $obj->isa('UR::DeletedRef'); $obj->__signal_change__('commit'); if ($obj->isa('UR::Object::Ghost')) { $UR::Context::current->_abandon_object($obj); } } return scalar(@$objects) || "0 but true"; } sub _set_object_saved_committed { # called by the above, and some test cases my ($self, $object) = @_; if ($object->{db_saved_uncommitted}) { unless ($object->isa('UR::Object::Ghost')) { %{ $object->{db_committed} } = ( ($object->{db_committed} ? %{ $object->{db_committed} } : ()), %{ $object->{db_saved_uncommitted} } ); delete $object->{db_saved_uncommitted}; } return $object; } else { return; } } sub _set_all_objects_saved_rolled_back { # called by UR::DBI on commit my $self = shift; my @objects = $self->_get_current_entities; for my $obj (@objects) { unless ($self->_set_object_saved_rolled_back($obj)) { die "An error occurred setting " . $obj->__display_name__ . " to match the rolled-back database state. Exiting..."; } } } sub _set_specified_objects_saved_rolled_back { my $self = shift; my $objects = shift; for my $obj (@$objects) { unless ($self->_set_object_saved_rolled_back($obj)) { die "An error occurred setting " . $obj->__display_name__ . " to match the rolled-back database state. Exiting..."; } } } sub _set_object_saved_rolled_back { # called by the above, and some test cases my ($self,$object) = @_; delete $object->{db_saved_uncommitted}; return $object; } # These are part of the basic DataSource API. Subclasses will want to override these sub _sync_database { my $class = shift; my %args = @_; $class = ref($class) || $class; $class->warning_message("Data source $class does not support saving objects to storage. " . scalar(@{$args{'changed_objects'}}) . " objects will not be saved"); return 1; } sub commit { my $class = shift; my %args = @_; $class = ref($class) || $class; #$class->warning_message("commit() ignored for data source $class"); return 1; } sub rollback { my $class = shift; my %args = @_; $class = ref($class) || $class; $class->warning_message("rollback() ignored for data source $class"); return 1; } # basic, dumb datasources do not have a handle sub get_default_handle { return; } # When the class initializer is create property objects, it will # auto-fill-in column_name if the class definition has a table_name. # File-based data sources do not have tables (and so classes using them # do not have table_names), but the properties still need column_names # so loading works properly. # For now, only UR::DataSource::File and ::FileMux set this. # FIXME this method's existence is ugly. Find a better way to fill in # column_name for those properties, or fix the data sources to not # require column_names to be set by the initializer sub initializer_should_create_column_name_for_class_properties { return 0; } # Subclasses should override this. # It's called by the class initializer when the data_source property in a class # definition contains a hashref with an 'is' key. The method should accept this # hashref, create a data_source instance (if appropriate) and return the class_name # of this new datasource. sub create_from_inline_class_data { my ($class,$class_data,$ds_data) = @_; my %ds_data = %$ds_data; my $ds_class_name = delete $ds_data{is}; unless (my $ds_class_meta = UR::Object::Type->get($ds_class_name)) { die "No class $ds_class_name found!"; } my $ds = $ds_class_name->__define__(%ds_data); unless ($ds) { die "Failed to construct $ds_class_name: " . $ds_class_name->error_message(); } return $ds; } sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; return [undef,undef]; # The default that should give reasonable behavior } # prepare_for_fork, do_after_fork_in_child, and finish_up_after_fork are no-op # here in the UR::DataSource base class and should be implented in subclasses # as needed. sub prepare_for_fork { return 1 } sub do_after_fork_in_child { return 1 } sub finish_up_after_fork { return 1 } 1; BoolExpr.pm000444023532023421 14277112121654173 15060 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::BoolExpr; use warnings; use strict; use Scalar::Util qw(blessed); require UR; use Carp; our @CARP_NOT = ('UR::Context'); our $VERSION = "0.41"; # UR $VERSION;; # readable stringification use overload ('""' => '__display_name__'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); UR::Object::Type->define( class_name => 'UR::BoolExpr', composite_id_separator => $UR::BoolExpr::Util::id_sep, id_by => [ template_id => { type => 'Blob' }, value_id => { type => 'Blob' }, ], has => [ template => { is => 'UR::BoolExpr::Template', id_by => 'template_id' }, subject_class_name => { via => 'template' }, logic_type => { via => 'template' }, logic_detail => { via => 'template' }, num_values => { via => 'template' }, is_normalized => { via => 'template' }, is_id_only => { via => 'template' }, has_meta_options => { via => 'template' }, ], is_transactional => 0, ); # for performance sub UR::BoolExpr::Type::resolve_composite_id_from_ordered_values { shift; return join($UR::BoolExpr::Util::id_sep,@_); } # only respect the first delimiter instead of splitting sub UR::BoolExpr::Type::resolve_ordered_values_from_composite_id { my ($self,$id) = @_; my $pos = index($id,$UR::BoolExpr::Util::id_sep); return (substr($id,0,$pos), substr($id,$pos+1)); } sub template { my $self = $_[0]; return $self->{template} ||= $self->__template; } sub flatten { my $self = shift; return $self->{flatten} if exists $self->{flatten}; my $flat = $self->template->_flatten_bx($self); $self->{flatten} = $flat; Scalar::Util::weaken($self->{flatten}) if $self == $flat; return $flat; } sub reframe { my $self = shift; my $in_terms_of = shift; return $self->{reframe}{$in_terms_of} if $self->{reframe}{$in_terms_of}; my $reframe = $self->template->_reframe_bx($self, $in_terms_of); $self->{reframe}{$in_terms_of} = $reframe; Scalar::Util::weaken($self->{reframe}{$in_terms_of}) if $self == $reframe; return $reframe; } # override the UR/system display name # this is used in stringification overload sub __display_name__ { my $self = shift; my %b = $self->_params_list; my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Dump; $s =~ s/\n/ /gs; $s =~ s/^\s*{//; $s =~ s/\}\s*$//; $s =~ s/\"(\w+)\" \=\> / $1 => /g; return __PACKAGE__ . '=(' . $self->subject_class_name . ':' . $s . ')'; } # The primary function: evaluate a subject object as matching the rule or not. sub evaluate { my $self = shift; my $subject = shift; my $template = $self->template; my @values = $self->values; return $template->evaluate_subject_and_values($subject,@values); } # Behind the id properties: sub template_and_values { my $self = shift; my ($template_id, $value_id) = UR::BoolExpr::Type->resolve_ordered_values_from_composite_id($self->id); return (UR::BoolExpr::Template->get($template_id), UR::BoolExpr::Util->value_id_to_values($value_id)); } # Returns true if the rule represents a subset of the things the other # rule would match. It returns undef if the answer is not known, such as # when one of the values is a list and we didn't go to the trouble of # searching the list for a matching value sub is_subset_of { my($self, $other_rule) = @_; return 0 unless (ref($other_rule) and $self->isa(ref $other_rule)); my $my_template = $self->template; my $other_template = $other_rule->template; unless ($my_template->isa("UR::BoolExpr::Template::And") and $other_template->isa("UR::BoolExpr::Template::And")) { Carp::confess("This method currently works only on ::And expressions. Update to handle ::Or, ::PropertyComparison, and templates of mismatched class!"); } return unless ($my_template->is_subset_of($other_template)); my $values_match = 1; foreach my $prop ( $other_template->_property_names ) { my $my_operator = $my_template->operator_for($prop) || '='; my $other_operator = $other_template->operator_for($prop) || '='; my $my_value = $self->value_for($prop); my $other_value = $other_rule->value_for($prop); # If either is a list of values, return undef return undef if (ref($my_value) || ref($other_value)); no warnings 'uninitialized'; $values_match = undef if ($my_value ne $other_value); } return $values_match; } sub values { my $self = shift; if ($self->{values}) { return @{ $self->{values}} } my $value_id = $self->value_id; return unless defined($value_id) and length($value_id); my @values; @values = UR::BoolExpr::Util->value_id_to_values($value_id); if (my $hard_refs = $self->{hard_refs}) { for my $n (keys %$hard_refs) { $values[$n] = $hard_refs->{$n}; } } $self->{values} = \@values; return @values; } sub value_for_id { my $self = shift; my $t = $self->template; my $position = $t->id_position; return unless defined $position; return $self->value_for_position($position); } sub specifies_value_for { my $self = shift; my $rule_template = $self->template; return $rule_template->specifies_value_for(@_); } sub value_for { my $self = shift; my $property_name = shift; # TODO: refactor to be more efficient my $template = $self->template; my $h = $self->legacy_params_hash; my $v; if (exists $h->{$property_name}) { # normal case $v = $h->{$property_name}; my $tmpl_pos = $template->value_position_for_property_name($property_name); if (exists $self->{'hard_refs'}->{$tmpl_pos}) { $v = $self->{'hard_refs'}->{$tmpl_pos}; # It was stored during resolve() as a hard ref } elsif ($self->_value_is_old_style_operator_and_value($v)) { $v = $v->{'value'}; # It was old style operator/value hash } } else { # No value found under that name... try decomposing the id return if $property_name eq 'id'; my $id_value = $self->value_for('id'); my $class_meta = $self->subject_class_name->__meta__(); my @id_property_values = $class_meta->get_composite_id_decomposer->($id_value); my @id_property_names = $class_meta->id_property_names; for (my $i = 0; $i < @id_property_names; $i++) { if ($id_property_names[$i] eq $property_name) { $v = $id_property_values[$i]; last; } } } return $v; } sub value_for_position { my ($self, $pos) = @_; return ($self->values)[$pos]; } sub operator_for { my $self = shift; my $t = $self->template; return $t->operator_for(@_); } sub underlying_rules { my $self = shift; unless (exists $self->{'_underlying_rules'}) { my @values = $self->values; $self->{'_underlying_rules'} = [ $self->template->get_underlying_rules_for_values(@values) ]; } return @{ $self->{'_underlying_rules'} }; } # De-compose the rule back into its original form. sub params_list { # This is the reverse of the bulk of resolve. # It returns the params in list form, directly coercable into a hash if necessary. # $r = UR::BoolExpr->resolve($c1,@p1); # ($c2, @p2) = ($r->subject_class_name, $r->params_list); my $self = shift; my $template = $self->template; my @values_sorted = $self->values; return $template->params_list_for_values(@values_sorted); } # TODO: replace these with the logical set operations # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object sub add_filter { my $self = shift; return __PACKAGE__->resolve($self->subject_class_name, $self->params_list, @_); } # TODO: replace these with the logical set operations # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object sub remove_filter { my $self = shift; my $property_name = shift; my @params_list = $self->params_list; my @new_params_list; for (my $n=0; $n<=$#params_list; $n+=2) { my $key = $params_list[$n]; if ($key =~ /^$property_name\b/) { next; } my $value = $params_list[$n+1]; push @new_params_list, $key, $value; } return __PACKAGE__->resolve($self->subject_class_name, @new_params_list); } # as above, doesn't mutate, just returns a different bx sub sub_classify { my ($self,$subclass_name) = @_; my ($t,@v) = $self->template_and_values(); return $t->sub_classify($subclass_name)->get_rule_for_values(@v); } # flyweight constructor # like regular UR::Value objects, but kept separate from the cache but kept # out of the regular transaction cache so they alwasy vaporize when derefed sub get { my $rule_id = pop; unless (exists $UR::Object::rules->{$rule_id}) { my $pos = index($rule_id,$UR::BoolExpr::Util::id_sep); my ($template_id,$value_id) = (substr($rule_id,0,$pos), substr($rule_id,$pos+1)); my $rule = { id => $rule_id, template_id => $template_id, value_id => $value_id }; bless ($rule, "UR::BoolExpr"); $UR::Object::rules->{$rule_id} = $rule; Scalar::Util::weaken($UR::Object::rules->{$rule_id}); return $rule; } return $UR::Object::rules->{$rule_id}; } # because these are weakened sub DESTROY { delete $UR::Object::rules->{$_[0]->{id}}; } sub flatten_hard_refs { my $self = $_[0]; return $self if not $self->{hard_refs}; my $subject_class_name = $self->subject_class_name; my $meta = $subject_class_name->__meta__; my %params = $self->_params_list; my $changes = 0; for my $key (keys %params) { my $value = $params{$key}; if (ref($value) and Scalar::Util::blessed($value) and $value->isa("UR::Object")) { my ($property_name,$op) = ($key =~ /^(\S+)\s*(.*)/); my $value_class_name = $meta->property($property_name)->data_type; next unless $value_class_name; my $id = $value->id; my $value2 = eval { $value_class_name->get($id) }; if (not $value2) { next; } if ($value2 == $value) { # safe to re-represent as .id my $new_key = $property_name . '.id'; $new_key .= ' ' . $op if $op; my $new_value = $value->id; delete $params{$key}; $params{$new_key} = $new_value; $changes++; } } } if ($changes) { return $self->resolve($subject_class_name, %params); } else { return $self; } } sub resolve_normalized { my $class = shift; my ($unnormalized_rule, @extra) = $class->resolve(@_); my $normalized_rule = $unnormalized_rule->normalize(); return if !defined(wantarray); return ($normalized_rule,@extra) if wantarray; if (@extra) { no warnings; my $rule_class = $normalized_rule->subject_class_name; Carp::confess("Extra params for class $rule_class found: @extra\n"); } return $normalized_rule; } sub resolve_for_template_id_and_values { my ($class,$template_id, @values) = @_; my $value_id = UR::BoolExpr::Util->values_to_value_id(@values); my $rule_id = $class->__meta__->resolve_composite_id_from_ordered_values($template_id,$value_id); $class->get($rule_id); } # Return true if it's a hashref that specifies the old-style operator/value # like property => { operator => '=', value => 1 } # FYI, the new way to do this is: # 'property =' => 1 sub _value_is_old_style_operator_and_value { my($class,$value) = @_; return (ref($value) eq 'HASH') && (exists($value->{'operator'})) && (exists($value->{'value'})) && ( (keys(%$value) == 2) || ((keys(%$value) == 3) && exists($value->{'escape'})) ); } my $resolve_depth; sub resolve { $resolve_depth++; Carp::confess("Deep recursion in UR::BoolExpr::resolve()!") if $resolve_depth > 10; # handle the case in which we've already processed the params into a boolexpr if ( @_ == 3 and ref($_[2]) and ref($_[2])->isa("UR::BoolExpr") ) { $resolve_depth--; return $_[2]; } my $class = shift; my $subject_class = shift; Carp::confess("Can't resolve BoolExpr: expected subject class as arg 2, got '$subject_class'") if not $subject_class; # support for legacy passing of hashref instead of object or list # TODO: eliminate the need for this my @in_params; if ($subject_class->isa('UR::Value::PerlReference') and $subject_class eq 'UR::Value::' . ref($_[0])) { @in_params = @_; } elsif (ref($_[0]) eq "HASH") { @in_params = %{$_[0]}; } else { @in_params = @_; } if (defined($in_params[0]) and $in_params[0] eq '-or') { shift @in_params; my @sub_queries = @{ shift @in_params }; my @meta_params; for (my $i = 0; $i < @in_params; $i += 2 ) { if ($in_params[$i] =~ m/^-/) { push @meta_params, $in_params[$i], $in_params[$i+1]; } } my $bx = UR::BoolExpr::Template::Or->_compose( $subject_class, \@sub_queries, \@meta_params, ); $resolve_depth--; return $bx; } if (@in_params == 1) { unshift @in_params, "id"; } elsif (@in_params % 2 == 1) { Carp::carp("Odd number of params while creating $class: (",join(',',@in_params),")"); } # split the params into keys and values # where an operator is on the right-side, it is moved into the key my $count = @in_params; my (@keys,@values,@constant_values,$key,$value,$property_name,$operator,@hard_refs); for(my $n = 0; $n < $count;) { $key = $in_params[$n++]; $value = $in_params[$n++]; unless (defined $key) { Carp::croak("Can't resolve BoolExpr: undef is an invalid key/property name. Args were: ".join(', ',@in_params)); } if (substr($key,0,1) eq '-') { # these are keys whose values live in the rule template push @keys, $key; push @constant_values, $value; next; } if ($key =~ m/^(_id_only|_param_key|_unique|__get_serial|_change_count)$/) { # skip the pair: legacy/internal cruft next; } my $pos = index($key,' '); if ($pos != -1) { # the key is "propname op" $property_name = substr($key,0,$pos); $operator = substr($key,$pos+1); if (substr($operator,0,1) eq ' ') { $operator =~ s/^\s+//; } } else { # the key is "propname" $property_name = $key; $operator = ''; } if (my $ref = ref($value)) { if ( (not $operator) and ($ref eq "HASH")) { if ( $class->_value_is_old_style_operator_and_value($value)) { # the key => { operator => $o, value => $v } syntax # cannot be used with a value type of HASH $operator = lc($value->{operator}); if (exists $value->{escape}) { $operator .= "-" . $value->{escape} } $key .= " " . $operator; $value = $value->{value}; $ref = ref($value); } else { # the HASH is a value for the specified param push @hard_refs, scalar(@values), $value; } } if ($ref eq "ARRAY") { if (not $operator) { # key => [] is the same as "key in" => [] $operator = 'in'; $key .= ' in'; } elsif ($operator eq 'not') { # "key not" => [] is the same as "key not in" $operator .= ' in'; $key .= ' in'; } foreach my $val (@$value) { if (ref($val)) { # when there are any refs in the arrayref # we must keep the arrayerf contents # to reconstruct effectively push @hard_refs, scalar(@values), $value; last; } } } # done handling ARRAY value } # done handling ref values push @keys, $key; push @values, $value; } # the above uses no class metadata # this next section uses class metadata # it should be moved into the normalization layer my $subject_class_meta = eval { $subject_class->__meta__ }; if ($@) { Carp::croak("Can't get class metadata for $subject_class. Is it a valid class name?\nErrors were: $@"); } unless ($subject_class_meta) { Carp::croak("No class metadata for $subject_class?!"); } my $subject_class_props = $subject_class_meta->{'cache'}{'UR::BoolExpr::resolve'} ||= { map {$_, 1} ( $subject_class_meta->all_property_type_names) }; my($kn, $vn, $cn, $complex_values) = (0,0,0,0); my ($op,@extra,@xadd_keys,@xadd_values,@xremove_keys,@xremove_values,@extra_key_pos,@extra_value_pos, @swap_key_pos,@swap_key_value,%in_clause_values_are_strings); for my $value (@values) { $key = $keys[$kn++]; if (substr($key,0,1) eq '-') { $cn++; redo; } else { $vn++; } my $pos = index($key,' '); if ($pos != -1) { # "propname op" $property_name = substr($key,0,$pos); $operator = substr($key,$pos+1); if (substr($operator,0,1) eq ' ') { $operator =~ s/^\s+//; } } else { # "propname" $property_name = $key; $operator = ''; } # account for the case where this parameter does # not match an actual property if (!exists $subject_class_props->{$property_name} and index($property_name,'.') == -1) { if (substr($property_name,0,1) eq '_') { warn "ignoring $property_name in $subject_class bx construction!" } else { push @extra_key_pos, $kn-1; push @extra_value_pos, $vn-1; next; } } my $ref = ref($value); if($ref) { $complex_values = 1; if ($ref eq "ARRAY" and $operator ne 'between' and $operator ne 'not between') { my $data_type; my $is_many; if ($UR::initialized) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless (defined $property_meta) { push @extra_key_pos, $kn-1; push @extra_value_pos, $vn-1; next; } $data_type = $property_meta->data_type; $is_many = $property_meta->is_many; } else { $data_type = $subject_class_meta->{has}{$property_name}{data_type}; $is_many = $subject_class_meta->{has}{$property_name}{is_many}; } $data_type ||= ''; if ($data_type eq 'ARRAY') { # ensure we re-constitute the original array not a copy push @hard_refs, $vn-1, $value; push @swap_key_pos, $vn-1; push @swap_key_value, $property_name; } elsif (not $is_many) { no warnings; # sort and replace # note that in perl5.10 and above strings like "inf*" have a numeric value # causing this kind of sorting to do surprising things. Hopefully looks_like_number() # does the right thing with these. # # undef/null sorts at the end my $sorter = sub { if (! defined($a)) { return 1 } if (! defined($b)) { return -1} return $a cmp $b; }; $value = [ sort $sorter @$value ]; # Remove duplicates from the list if ($operator ne 'between' and $operator ne 'not between') { my $last = $value; for (my $i = 0; $i < @$value;) { if ($last eq $value->[$i]) { splice(@$value, $i, 1); } else { $last = $value->[$i++]; } } } # push @swap_key_pos, $vn-1; # push @swap_key_value, $property_name; } else { # disable: break 47, enable: break 62 #push @swap_key_pos, $vn-1; #push @swap_key_value, $property_name; } } elsif (blessed($value)) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless ($property_meta) { for my $class_name ($subject_class_meta->ancestry_class_names) { my $class_object = $class_name->__meta__; $property_meta = $subject_class_meta->property_meta_for_name($property_name); last if $property_meta; } unless ($property_meta) { Carp::croak("No property metadata for $subject_class property '$property_name'"); } } if ($property_meta->id_by or $property_meta->reverse_as) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); unless ($property_meta) { Carp::croak("No property metadata for $subject_class property '$property_name'"); } my @joins = $property_meta->get_property_name_pairs_for_join(); for my $join (@joins) { # does this really work for >1 joins? my ($my_method, $their_method) = @$join; push @xadd_keys, $my_method; push @xadd_values, $value->$their_method; } # TODO: this may need to be moved into the above get_property_name_pairs_for_join(), # but the exact syntax for expressing that this is part of the join is unclear. if (my $id_class_by = $property_meta->id_class_by) { push @xadd_keys, $id_class_by; push @xadd_values, ref($value); #print ":: @xkeys\n::@xvalues\n\n"; } push @xremove_keys, $kn-1; push @xremove_values, $vn-1; } # This is disabled here because it is good for get() but not create() # The flatten_hard_refs() method is run before doing a get() to create the same effect. # elsif ($property_meta->is_delegated and not $property_meta->is_many) { # print STDERR "adding $property_name.id\n"; # push @xadd_keys, $property_name . '.id' . ' ' . $operator; # push @xadd_values, $value->id; # push @xremove_keys, $kn-1; # push @xremove_values, $vn-1; # } elsif ($property_meta->is_valid_storage_for_value($value)) { push @hard_refs, $vn-1, $value; } elsif ($value->can($property_name)) { # TODO: stop suporting foo_id => $foo, since you can do foo=>$foo, and foo_id=>$foo->id #$DB::single = 1; # Carp::cluck("using $property_name => \$obj to get $property_name => \$obj->$property_name is deprecated..."); $value = $value->$property_name; } else { $operator = 'eq' unless $operator; $DB::single = 1; print $value->isa($property_meta->_data_type_as_class_name),"\n"; print $value->isa($property_meta->_data_type_as_class_name),"\n"; Carp::croak("Invalid data type in rule. A value of type " . ref($value) . " cannot be used in class $subject_class property '$property_name' with operator $operator!"); } # end of handling a value which is an arrayref } elsif ($ref ne 'HASH') { # other reference, code, etc. push @hard_refs, $vn-1, $value; } } } push @keys, @xadd_keys; push @values, @xadd_values; if (@swap_key_pos) { @keys[@swap_key_pos] = @swap_key_value; } if (@extra_key_pos) { push @xremove_keys, @extra_key_pos; push @xremove_values, @extra_value_pos; for (my $n = 0; $n < @extra_key_pos; $n++) { push @extra, $keys[$extra_key_pos[$n]], $values[$extra_value_pos[$n]]; } } if (@xremove_keys) { my @new; my $next_pos_to_remove = $xremove_keys[0]; for (my $n = 0; $n < @keys; $n++) { if (defined $next_pos_to_remove and $n == $next_pos_to_remove) { shift @xremove_keys; $next_pos_to_remove = $xremove_keys[0]; next; } push @new, $keys[$n]; } @keys = @new; } if (@xremove_values) { if (@hard_refs) { # shift the numbers down to account for positional removals for (my $n = 0; $n < @hard_refs; $n+=2) { my $ref_pos = $hard_refs[$n]; for my $rem_pos (@xremove_values) { if ($rem_pos < $ref_pos) { $hard_refs[$n] -= 1; #print "$n from $ref_pos to $hard_refs[$n]\n"; $ref_pos = $hard_refs[$n]; } elsif ($rem_pos == $ref_pos) { $hard_refs[$n] = ''; $hard_refs[$n+1] = undef; } } } } my @new; my $next_pos_to_remove = $xremove_values[0]; for (my $n = 0; $n < @values; $n++) { if (defined $next_pos_to_remove and $n == $xremove_values[0]) { shift @xremove_values; $next_pos_to_remove = $xremove_values[0]; next; } push @new, $values[$n]; } @values = @new; } my $template; if (@constant_values) { $template = UR::BoolExpr::Template::And->_fast_construct( $subject_class, \@keys, \@constant_values, ); } else { $template = $subject_class_meta->{cache}{"UR::BoolExpr::resolve"}{"template for class and keys without constant values"}{"$subject_class @keys"} ||= UR::BoolExpr::Template::And->_fast_construct( $subject_class, \@keys, \@constant_values, ); } my $value_id = ($complex_values ? UR::BoolExpr::Util->values_to_value_id(@values) : UR::BoolExpr::Util->values_to_value_id_simple(@values) ); my $rule_id = join($UR::BoolExpr::Util::id_sep,$template->{id},$value_id); my $rule = __PACKAGE__->get($rule_id); # flyweight constructor $rule->{template} = $template; $rule->{values} = \@values; $rule->{_in_clause_values_are_strings} = \%in_clause_values_are_strings if (keys %in_clause_values_are_strings); $vn = 0; $cn = 0; my @list; for my $key (@keys) { push @list, $key; if (substr($key,0,1) eq '-') { push @list, $constant_values[$cn++]; } else { push @list, $values[$vn++]; } } $rule->{_params_list} = \@list; if (@hard_refs) { $rule->{hard_refs} = { @hard_refs }; delete $rule->{hard_refs}{''}; } $resolve_depth--; if (wantarray) { return ($rule, @extra); } elsif (@extra && defined wantarray) { Carp::confess("Unknown parameters in rule for $subject_class: " . join(",", map { defined($_) ? "'$_'" : "(undef)" } @extra)); } else { return $rule; } } sub _params_list { my $list = $_[0]->{_params_list} ||= do { my $self = $_[0]; my $template = $self->template; $self->values unless $self->{values}; my @list; # are method calls really too expensive here? my $template_class = ref($template); if ($template_class eq 'UR::BoolExpr::Template::And') { my ($k,$v,$c) = ($template->{_keys}, $self->{values}, $template->{_constant_values}); my $vn = 0; my $cn = 0; for my $key (@$k) { push @list, $key; if (substr($key,0,1) eq '-') { push @list, $c->[$cn++]; } else { push @list, $v->[$vn++]; } } } elsif ($template_class eq 'UR::BoolExpr::Template::Or') { my @sublist; my @u = $self->underlying_rules(); for my $u (@u) { my @p = $u->_params_list; push @sublist, \@p; } @list = (-or => \@sublist); } elsif ($template_class->isa("UR::BoolExpr::PropertyComparison")) { @list = ($template->logic_detail => [@{$self->{values}}]); } \@list; }; return @$list; } sub normalize { my $self = shift; my $rule_template = $self->template; if ($rule_template->{is_normalized}) { return $self; } my @unnormalized_values = $self->values(); my $normalized = $rule_template->get_normalized_rule_for_values(@unnormalized_values); return unless defined $normalized; if (my $special = $self->{hard_refs}) { $normalized->{hard_refs} = $rule_template->_normalize_non_ur_values_hash($special); } return $normalized; } # a handful of places still use this sub legacy_params_hash { my $self = shift; # See if we have one already. my $params_array = $self->{legacy_params_array}; return { @$params_array } if $params_array; # Make one by starting with the one on the rule template my $rule_template = $self->template; my $params = { %{$rule_template->legacy_params_hash}, $self->params_list }; # If the template has a _param_key, fill it in. if (exists $params->{_param_key}) { $params->{_param_key} = $self->id; } # This was cached above and will return immediately on the next call. # Note: the caller should copy this reference before making changes. $self->{legacy_params_array} = [ %$params ]; return $params; } my $LOADED_BXPARSE = 0; sub resolve_for_string { my ($class, $subject_class_name, $filter_string, $usage_hints_string, $order_string, $page_string) = @_; unless ($LOADED_BXPARSE) { eval { require UR::BoolExpr::BxParser }; if ($@) { Carp::croak("resolve_for_string() can't load UR::BoolExpr::BxParser: $@"); } $LOADED_BXPARSE=1; } #$DB::single=1; #my $tree = UR::BoolExpr::BxParser::parse($filter_string, tokdebug => 1, yydebug => 7); my($tree, $remaining_strref) = UR::BoolExpr::BxParser::parse($filter_string); unless ($tree) { Carp::croak("resolve_for_string() couldn't parse string \"$filter_string\""); } push @$tree, '-hints', [split(',',$usage_hints_string) ] if ($usage_hints_string); push @$tree, '-order_by', [split(',',$order_string) ] if ($order_string); push @$tree, '-page', [split(',',$page_string) ] if ($page_string); my $bx = UR::BoolExpr->resolve($subject_class_name, @$tree); unless ($bx) { Carp::croak("Can't create BoolExpr on $subject_class_name from params generated from string " . $filter_string . " which parsed as:\n" . Data::Dumper::Dumper($tree)); } if ($$remaining_strref) { Carp::croak("Trailing input after the parsable end of the filter string: '". $$remaining_strref."'"); } return $bx; } # TODO: these methods need a better home, since they are a cmdline/UI standard sub _old_filter_regex_for_string { return '^\s*([\w\.\-]+)\s*(\@|\=|!=|=|\>|\<|~|!~|!\:|\:|\blike\b|\bbetween\b|\bin\b)\s*[\'"]?([^\'"]*)[\'"]?\s*$'; } # TODO: these methods need a better home, since they are a cmdline/UI standard sub _old_resolve_for_string { my ($self, $subject_class_name, $filter_string, $usage_hints_string, $order_string, $page_string) = @_; my ($property, $op, $value); no warnings; my $filter_regex = $self->_old_filter_regex_for_string(); my @filters = map { unless (($property, $op, $value) = ($_ =~ /$filter_regex/)) { Carp::croak "Unable to process filter $_\n"; } if ($op eq '~') { $op = "like"; # If the user asked for 'like', but didn't put in a wildcard, then put wildcards # on each side of the value $value = '%'.$value.'%' if (length($value) and $value !~ m/\%|_/); } elsif ($op eq '!~') { $op = 'not like'; $value = '%'.$value.'%' if (length($value) and $value !~ m/\%|_/); } [$property, $op, $value] } split(/,/, $filter_string); my @hints = split(",",$usage_hints_string); my @order = split(",",$order_string); my @page = split(",",$page_string); use warnings; return __PACKAGE__->_resolve_from_filter_array($subject_class_name, \@filters, \@hints, \@order, \@page); } sub _resolve_from_filter_array { my $class = shift; my $subject_class_name = shift; my $filters = shift; my $usage_hints = shift; my $order = shift; my $page = shift; my @rule_filters; my @keys; my @values; for my $fdata (@$filters) { my $rule_filter; # rule component my $key = $fdata->[0]; my $value; # process the operator if ($fdata->[1] =~ /^!?(:|@|between|in)$/i) { my @list_parts; my @range_parts; if ($fdata->[1] eq "@") { # file path my $fh = IO::File->new($fdata->[2]); unless ($fh) { die "Failed to open file $fdata->[2]: $!\n"; } @list_parts = $fh->getlines; chomp @list_parts; $fh->close; } else { @list_parts = split(/\//,$fdata->[2]); @range_parts = split(/-/,$fdata->[2]); } if (@list_parts > 1) { my $op = ($fdata->[1] =~ /^!/ ? 'not in' : 'in'); # rule component if (substr($key, -3, 3) ne ' in') { $key = join(' ', $key, $op); } $value = \@list_parts; $rule_filter = [$fdata->[0],$op,\@list_parts]; } elsif (@range_parts >= 2) { if (@range_parts > 2) { if (@range_parts % 2) { die "The \":\" operator expects a range sparated by a single dash: @range_parts ." . "\n"; } else { my $half = (@range_parts)/2; $a = join("-",@range_parts[0..($half-1)]); $b = join("-",@range_parts[$half..$#range_parts]); } } elsif (@range_parts == 2) { ($a,$b) = @range_parts; } else { die 'The ":" operator expects a range sparated by a dash.' . "\n"; } $key = $fdata->[0] . " between"; $value = [$a, $b]; $rule_filter = [$fdata->[0], "between", [$a, $b] ]; } else { die 'The ":" operator expects a range sparated by a dash, or a slash-separated list.' . "\n"; } } # this accounts for cases where value is null elsif (length($fdata->[2])==0) { if ($fdata->[1] eq "=") { $key = $fdata->[0]; $value = undef; $rule_filter = [ $fdata->[0], "=", undef ]; } else { $key = $fdata->[0] . " !="; $value = undef; $rule_filter = [ $fdata->[0], "!=", undef ]; } } else { $key = $fdata->[0] . ($fdata->[1] and $fdata->[1] ne '='? ' ' . $fdata->[1] : ''); $value = $fdata->[2]; $rule_filter = [ @$fdata ]; } push @keys, $key; push @values, $value; } if ($usage_hints or $order or $page) { # todo: incorporate hints in a smarter way my %p; for my $key (@keys) { $p{$key} = shift @values; } return $class->resolve( $subject_class_name, %p, ($usage_hints ? (-hints => $usage_hints) : () ), ($order ? (-order => $order) : () ), ($page ? (-page => $page) : () ), ); } else { return UR::BoolExpr->_resolve_from_subject_class_name_keys_and_values( subject_class_name => $subject_class_name, keys => \@keys, values=> \@values, ); } } sub _resolve_from_subject_class_name_keys_and_values { my $class = shift; my %params = @_; my $subject_class_name = $params{subject_class_name}; my @values = @{ $params{values} || [] }; my @constant_values = @{ $params{constant_values} || [] }; my @keys = @{ $params{keys} || [] }; die "unexpected params: " . Data::Dumper::Dumper(\%params) if %params; my $value_id = UR::BoolExpr::Util->values_to_value_id(@values); my $constant_value_id = UR::BoolExpr::Util->values_to_value_id(@constant_values); my $template_id = $subject_class_name . '/And/' . join(",",@keys) . "/" . $constant_value_id; my $rule_id = join($UR::BoolExpr::Util::id_sep,$template_id,$value_id); my $rule = __PACKAGE__->get($rule_id); $rule->{values} = \@values; return $rule; } 1; =pod =head1 NAME UR::BoolExpr - a "where clause" for objects =head1 SYNOPSIS my $o = Acme::Employee->create( ssn => '123-45-6789', name => 'Pat Jones', status => 'active', start_date => UR::Context->current->now, payroll_category => 'hourly', boss => $other_employee, ); my $bx = Acme::Employee->define_boolexpr( 'payroll_category' => 'hourly', 'status' => ['active','terminated'], 'name like' => '%Jones', 'ssn matches' => '\d{3}-\d{2}-\d{4}', 'start_date between' => ['2009-01-01','2009-02-01'], 'boss.name in' => ['Cletus Titus', 'Mitzy Mayhem'], ); $bx->evaluate($o); # true $bx->specifies_value_for('payroll_category') # true $bx->value_for('payroll_cagtegory') # 'hourly' $o->payroll_category('salary'); $bx->evaluate($o); # false # these could take either a boolean expression, or a list of params # from which it will generate one on-the-fly my $set = Acme::Employee->define_set($bx); # same as listing all of the params my @matches = Acme::Employee->get($bx); # same as above, but returns the members my $bx2 = $bx->reframe('boss'); #'employees.payroll_category' => 'hourly', #'employees.status' => ['active','terminated'], #'employees.name like' => '%Jones', #'employees.ssn matches' => '\d{3}-\d{2}-\d{4}', #'employees.start_date between' => ['2009-01-01','2009-02-01'], #'name in' => ['Cletus Titus', 'Mitzy Mayhem'], my $bx3 = $bx->flatten(); # any indirection in the params takes the form a.b.c at the lowest level # also 'payroll_category' might become 'pay_history.category', and 'pay_history.is_current' => 1 is added to the list # if this parameter has that as a custom filter =head1 DESCRIPTION A UR::BoolExpr object captures a set of match criteria for some class of object. Calls to get(), create(), and define_set() all use this internally to objectify their parameters. If given a boolean expression object directly they will use it. Otherwise they will construct one from the parameters given. They have a 1:1 correspondence within the WHERE clause in an SQL statement where RDBMS persistance is used. They also imply the FROM clause in these cases, since the query properties control which joins must be included to return the matching object set. =head1 REFLECTION The data used to create the boolean expression can be re-extracted: my $c = $r->subject_class_name; # $c eq "GSC::Clone" my @p = $r->params_list; # @p = four items my %p = $r->params_list; # %p = two key value pairs =head1 TEMPLATE SUBCLASSES The template behind the expression can be of type ::Or, ::And or ::PropertyComparison. These classes handle all of the operating logic for the expressions. Each of those classes incapsulates 0..n of the next type in the list. All templates simplify to this level. See L for details. =head1 CONSTRUCTOR =over 4 my $bx = UR::BoolExpr->resolve('Some::Class', property_1 => 'value_1', ... property_n => 'value_n'); my $bx1 = Some::Class->define_boolexpr(property_1 => value_1, ... property_n => 'value_n'); my $bx2 = Some::Class->define_boolexpr('property_1 >' => 12345); my $bx3 = UR::BoolExpr->resolve_for_string( 'Some::Class', 'property_1 = value_1 and ( property_2 < value_2 or property_3 = value_3 )', ); Returns a UR::BoolExpr object that can be used to perform tests on the given class and properties. The default comparison for each property is equality. The third example shows using greater-than operator for property_1. The last example shows constructing a UR::BoolExpr from a string containing properties, operators and values joined with 'and' and 'or', with parentheses indicating precedence. =back C can parse simple and complicated expressions. A simple expression is a property name followed by an operator followed by a value. The property name can be a series of properties joined by dots (.) to indicate traversal of multiple layers of indirect properties. Values that include spaces, characters that look like operators, commas, or other special characters should be enclosed in quotes. The parser understands all the same operators the underlying C method understands: =, <, >, <=, >=, "like", "between" and "in". Operators may be prefixed by a bang (!) or the word "not" to negate the operator. The "like" operator understands the SQL wildcards % and _. Values for the "between" operator should be separated by a minus (-). Values for the "in" operator should begin with a left bracket, end with a right bracket, and have commas between them. For example: name_property in [Bob,Fred,Joe] Simple expressions may be joined together with the words "and" and "or" to form a more complicated expression. "and" has higher precedence than "or", and parentheses can surround sub-expressions to indicate the requested precedence. For example: ((prop1 = foo or prop2 = 1) and (prop2 > 10 or prop3 like 'Yo%')) or prop4 in [1,2,3] In general, whitespace is insignificant. The strings "prop1 = 1" is parsed the same as "prop1=1". Spaces inside quoted value strings are preserved. For backward compatibility with the deprecated string parser, bare words that appear after the operators =,<,>,<= and >= which are separated by one or more spaces is treated as if it had quotes around the list of words starting with the first character of the first word and ending with the last character of the last word, meaning that spaces at the start and end of the list are trimmed. Specific ordering may be requested by putting an "order by" clause at the end, and is the same as using a -order argument to resolve(): score > 10 order by name,score. Likewise, grouping and Set construction is indicated with a "group by" clause: score > 10 group by color =head1 METHODS =over 4 =item evaluate $bx->evaluate($object) Returns true if the given object satisfies the BoolExpr =item template_and_values ($template, @values) = $bx->template_and_values(); Returns the UR::BoolExpr::Template and list of the values for the given BoolExpr =item is_subset_of $bx->is_subset_of($other_bx) Returns true if the set of objects that matches this BoolExpr is a subset of the set of objects that matches $other_bx. In practice this means: * The subject class of $bx isa the subject class of $other_bx * all the properties from $bx also appear in $other_bx * the operators and values for $bx's properties match $other_bx =item values @values = $bx->values Return a list of the values from $bx. The values will be in the same order the BoolExpr was created from =item value_for_id $id = $bx->value_for_id If $bx's properties include all the ID properties of its subject class, C returns that value. Otherwise, it returns the empty list. If the subject class has more than one ID property, this returns the value of the composite ID. =item specifies_value_for $bx->specifies_value_for('property_name'); Returns true if the filter list of $bx includes the given property name =item value_for my $value = $bx->value_for('property_name'); Return the value for the given property =item operator_for my $operator = $bx->operator_for('property_name'); Return a string for the operator of the given property. A value of '' (the empty string) means equality ("="). Other possible values inclue '<', '>', '<=', '>=', 'between', 'true', 'false', 'in', 'not <', 'not >', etc. =item normalize $bx2 = $bx->normalize; A boolen expression can be changed in incidental ways and still be equivalent. This method converts the expression into a normalized form so that it can be compared to other normalized expressions without incidental differences affecting the comparision. =item flatten $bx2 = $bx->flatten(); Transforms a boolean expression into a functional equivalent where indirect properties are turned into property chains. For instance, in a class with a => { is => "A", id_by => "a_id" }, b => { via => "a", to => "bb" }, c => { via => "b", to => "cc" }, An expression of: c => 1234 Becomes: a.bb.cc => 1234 In cases where one of the indirect properties includes a "where" clause, the flattened expression would have an additional value for each element: a => { is => "A", id_by => "a_id" }, b => { via => "a", to => "bb" }, c => { via => "b", where ["xx" => 5678], to => "cc" }, An expression of: c => 1234 Becomes: a.bb.cc => 1234 a.bb.xx => 5678 =item reframe $bx = Acme::Order->define_boolexpr(status => 'active'); $bx2 = $bx->reframe('customer'); The above will turn a query for orders which are active into a query for customers with active orders, presuming an Acme::Order has a property called "customer" with a defined relationship to another class. =back =head1 INTERNAL STRUCTURE A boolean expression (or "rule") has an "id", which completely describes the rule in stringified form, and a method called evaluate($o) which tests the rule on a given object. The id is composed of two parts: - A template_id. - A value_id. Nearly all real work delegates to the template to avoid duplication of cached details. The template_id embeds several other properties, for which the rule delegates to it: - subject_class_name, objects of which the rule can be applied-to - subclass_name, the subclass of rule (property comparison, and, or "or") - the body of the rule either key-op-val, or a list of other rules For example, the rule GSC::Clone name=x,chromosome>y: - the template_id embeds: subject_class_name = GSC::Clone subclass_name = UR::BoolExpr::And and the key-op pairs in sorted order: "chromosome>,name=" - the value_id embeds the x,y values in a special format =head1 EXAMPLES my $bool = $x->evaluate($obj); my $t = GSC::Clone->template_for_params( "status =", "chromosome []", "clone_name like", "clone_size between" ); my @results = $t->get_matching_objects( "active", [2,4,7], "Foo%", [100000,200000] ); my $r = $t->get_rule($v1,$v2,$v3); my $t = $r->template; my @results = $t->get_matching_objects($v1,$v2,$v3); my @results = $r->get_matching_objects(); @r = $r->underlying_rules(); for (@r) { print $r->evaluate($c1); } my $rt = $r->template(); my @rt = $rt->get_underlying_rule_templates(); $r = $rt->get_rule_for_values(@v); $r = UR::BoolExpr->resolve_for_string( 'My::Class', 'name=Bob and (score=10 or score < 5)', ); =head1 SEE ALSO UR(3), UR::Object(3), UR::Object::Set(3), UR::BoolExpr::Template(3) =cut ModuleConfig.pm000444023532023421 1651112121654173 15651 0ustar00abrummetgsc000000000000UR-0.41/lib/UR# Manage dynamic configuration of modules. package UR::ModuleConfig; =pod =head1 NAME UR::ModuleConfig - manage dynamic configuration of modules. =head1 SYNOPSIS package MyModule; use base qw(UR::ModuleConfig); MyModule->config(%conf); $val = MyModule->config('key'); %conf = MyModule->config; =head1 DESCRIPTION This module manages the configuration for modules. Configurations can be read from files or set dynamically. Modules wishing to use the configuration methods should inherit from the module. =cut # set up package require 5.006_000; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; use base qw(UR::ModuleBase); use IO::File; =pod =head2 METHODS The methods deal with managing configuration. =cut # hash containing all configuration information our %config; # create a combined configuration hash from inheritance tree sub _inherit_config { my $self = shift; my $class = ref($self) || $self; my %cfg; # get all packages inherited from my @inheritance = $self->inheritance; # reverse loop through inheritance tree and construct config foreach my $cls (reverse(@inheritance)) { if (exists($config{$cls})) { # add hash, overriding previous values %cfg = (%cfg, %{$config{$cls}}); } } # now add the current class config if (exists($config{$class})) { %cfg = (%cfg, %{$config{$class}}); } # now add the object config if (ref($self)) { # add the objects config if (exists($config{"$class;$self"})) { %cfg = (%cfg, %{$config{"$class;$self"}}); } } return %cfg; } =pod =over 4 =item config MyModule->config(%config); $val = MyModule->config('key'); %conf = MyModule->config; my $obj = MyModule->new; $obj->config(%config); This method can be called three ways, as either a class or object method. The first method takes a hash as its argument and sets the configuration parameters given in the hash. The second method takes a single argument which should be one of the keys of the hash that set the config parameters and returns the value of that config hash key. The final method takes no arguments and returns the entire configuration hash. When called as an object method, the config for both the object and all classes in its inheritance hierarchy are referenced, with the object config taking precedence over class methods and class methods closer to the object (first in the @ISA array) taking precedence over those further away (later in the @ISA array). When called as a class method, the same procedure is used, except no object configuration is referenced. Do not use configuration keys that begin with an underscore (C<_>). These are reserved for internal use. =cut sub config { my $self = shift; my $class = ref($self) || $self; # handle both object and class configuration my $target; if (ref($self)) { # object config $target = "$class;$self"; } else { # class config $target = $self; } # lay claim to the modules configuration $config{$target}{_Manager} = __PACKAGE__; # see if values are being set if (@_ > 1) { # set values in config hash, overriding any current values my (%opts) = @_; %{$config{$target}} = (%{$config{$target}}, %opts); return 1; } # else they want one key or the whole hash # store config for object and inheritance tree my %cfg = $self->_inherit_config; # see how we were called if (@_ == 1) { # return value of key my ($key) = @_; # make sure hash key exists my $val; if (exists($cfg{$key})) { $self->debug_message("config key $key exists"); $val = $cfg{$key}; } else { $self->error_message("config key $key does not exist"); return; } return $val; } # else return the entire config hash return %cfg; } =pod =item check_config $obj->check_config($key); This method checks to see if a value is set. Unlike config, it does not issue a warning if the key is not set. If the key is not set, C is returned. If the key has been set, the value of the key is returned (which may be C). =cut sub check_config { my $self = shift; my ($key) = @_; # get config for inheritance tree my %cfg = $self->_inherit_config; if (exists($cfg{$key})) { $self->debug_message("configuration key $key set: $cfg{$key}"); return $cfg{$key}; } # else $self->debug_message("configuration key $key not set"); return; } =pod =item default_config $class->default_config(%defaults); This method allows the developer to set configuration values, only if they are not already set. =cut sub default_config { my $self = shift; my (%opts) = @_; # get config for inheritance tree my %cfg = $self->_inherit_config; # loop through arguments while (my ($k, $v) = each(%opts)) { # see is config value is already set if (exists($cfg{$k})) { $self->debug_message("config $k already set"); next; } $self->debug_message("setting default for $k"); # set config key $self->config($k => $v); } return 1; } =pod =item config_file $rv = $class->config_file(path => $path); $rv = $class->config_file(handle => $fh); This method reads in the given file and expects key-value pairs, one per line. The key and value should be separated by an equal sign, C<=>, with optional surrounding space. It currently only handles single value values. The method returns true upon success, C on failure. =cut sub config_file { my $self = shift; my (%opts) = @_; my $fh; if ($opts{path}) { # make sure file is ok if (-f $opts{path}) { $self->debug_message("config file exists: $opts{path}"); } else { $self->error_message("config file does not exist: $opts{path}"); return; } if (-r $opts{path}) { $self->debug_message("config file is readable: $opts{path}"); } else { $self->error_message("config file is not readable: $opts{path}"); return; } # open file $fh = IO::File->new("<$opts{path}"); if (defined($fh)) { $self->debug_message("opened config file for reading: $opts{path}"); } else { $self->error_message("failed to open config file for reading: " . $opts{path}); return; } } elsif ($opts{handle}) { $fh = $opts{handle}; } else { $self->error_message("no config file input specified"); return; } # read through file my %fconfig; while (defined(my $line = $fh->getline)) { # clean up chomp($line); $line =~ s/\#.*//; $line =~ s/^\s*//; $line =~ s/\s*$//; next unless $line =~ m/\S/; # parse my ($k, $v) = split(m/\s*=\s*/, $line, 2); $fconfig{$k} = $v; } $fh->close; # update config return $self->config(%fconfig); } 1; #$Header$ All.pm000444023532023421 2127612121654174 14013 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::All; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; BEGIN { require above; }; use UR; use Command; use Command::DynamicSubCommands; use Command::Test; use Command::Test::Echo; use Command::Test::Tree1; use Command::Test::Tree1::Echo1; use Command::Test::Tree1::Echo2; use Command::Tree; use Command::V1; use Command::V2; use Devel::callcount; use UR::BoolExpr; use UR::BoolExpr::Template; use UR::BoolExpr::Template::And; use UR::BoolExpr::Template::Composite; use UR::BoolExpr::Template::Or; use UR::BoolExpr::Template::PropertyComparison; use UR::BoolExpr::Template::PropertyComparison::Between; use UR::BoolExpr::Template::PropertyComparison::Equals; use UR::BoolExpr::Template::PropertyComparison::False; use UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual; use UR::BoolExpr::Template::PropertyComparison::GreaterThan; use UR::BoolExpr::Template::PropertyComparison::In; use UR::BoolExpr::Template::PropertyComparison::LessOrEqual; use UR::BoolExpr::Template::PropertyComparison::LessThan; use UR::BoolExpr::Template::PropertyComparison::Like; use UR::BoolExpr::Template::PropertyComparison::Matches; use UR::BoolExpr::Template::PropertyComparison::NotEqual; use UR::BoolExpr::Template::PropertyComparison::NotIn; use UR::BoolExpr::Template::PropertyComparison::NotLike; use UR::BoolExpr::Template::PropertyComparison::True; use UR::BoolExpr::Util; use UR::Change; use UR::Context; use UR::Context::DefaultRoot; use UR::Context::ObjectFabricator; use UR::Context::Process; use UR::Context::Root; use UR::Context::Transaction; use UR::DataSource; use UR::DataSource::Code; use UR::DataSource::CSV; use UR::DataSource::Default; use UR::DataSource::File; use UR::DataSource::FileMux; use UR::DataSource::Meta; BEGIN { eval { require DBD::mysql }; require UR::DataSource::MySQL unless $@; } BEGIN { eval { require DBD::Oracle }; require UR::DataSource::Oracle unless $@; } BEGIN { eval { require DBD::Pg }; require UR::DataSource::Pg unless $@; } use UR::DataSource::RDBMS; use UR::DataSource::RDBMS::BitmapIndex; use UR::DataSource::RDBMS::Entity; use UR::DataSource::RDBMS::FkConstraint; use UR::DataSource::RDBMS::FkConstraintColumn; use UR::DataSource::RDBMS::PkConstraintColumn; use UR::DataSource::RDBMS::Table; use UR::DataSource::RDBMS::Table::View::Default::Text; use UR::DataSource::RDBMS::TableColumn; use UR::DataSource::RDBMS::TableColumn::View::Default::Text; use UR::DataSource::RDBMS::UniqueConstraintColumn; use UR::DataSource::SQLite; use UR::DataSource::ValueDomain; use UR::DBI; use UR::Debug; use UR::DeletedRef; use UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES; use UR::Env::UR_CONTEXT_BASE; use UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER; use UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER; use UR::Env::UR_CONTEXT_MONITOR_QUERY; use UR::Env::UR_CONTEXT_ROOT; use UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT; use UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK; use UR::Env::UR_DBI_EXPLAIN_SQL_IF; use UR::Env::UR_DBI_EXPLAIN_SQL_MATCH; use UR::Env::UR_DBI_EXPLAIN_SQL_SLOW; use UR::Env::UR_DBI_MONITOR_DML; use UR::Env::UR_DBI_MONITOR_EVERY_FETCH; use UR::Env::UR_DBI_MONITOR_SQL; use UR::Env::UR_DBI_NO_COMMIT; use UR::Env::UR_DEBUG_OBJECT_PRUNING; use UR::Env::UR_DEBUG_OBJECT_RELEASE; use UR::Env::UR_IGNORE; use UR::Env::UR_NR_CPU; use UR::Env::UR_STACK_DUMP_ON_DIE; use UR::Env::UR_STACK_DUMP_ON_WARN; use UR::Env::UR_TEST_FILLDB; use UR::Env::UR_TEST_QUIET; use UR::Env::UR_USE_ANY; use UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS; use UR::Env::UR_USED_LIBS; use UR::Env::UR_USED_MODS; use UR::Exit; use UR::ModuleBase; use UR::ModuleBuild; use UR::ModuleConfig; use UR::ModuleLoader; use UR::Namespace; use UR::Namespace::Command; use UR::Namespace::Command::Base; use UR::Namespace::Command::Define; use UR::Namespace::Command::Define::Class; use UR::Namespace::Command::Define::Datasource; use UR::Namespace::Command::Define::Datasource::File; use UR::Namespace::Command::Define::Datasource::Mysql; use UR::Namespace::Command::Define::Datasource::Oracle; use UR::Namespace::Command::Define::Datasource::Pg; use UR::Namespace::Command::Define::Datasource::Rdbms; use UR::Namespace::Command::Define::Datasource::RdbmsWithAuth; use UR::Namespace::Command::Define::Datasource::Sqlite; use UR::Namespace::Command::Define::Db; use UR::Namespace::Command::Define::Namespace; use UR::Namespace::Command::Show::Properties; use UR::Namespace::Command::Show::Schema; use UR::Namespace::Command::Init; use UR::Namespace::Command::List; use UR::Namespace::Command::List::Classes; use UR::Namespace::Command::List::Modules; use UR::Namespace::Command::List::Objects; use UR::Namespace::Command::Old; use UR::Namespace::Command::Old::DiffRewrite; use UR::Namespace::Command::Old::DiffUpdate; use UR::Namespace::Command::Old::ExportDbicClasses; use UR::Namespace::Command::Old::Info; use UR::Namespace::Command::Old::Redescribe; use UR::Namespace::Command::RunsOnModulesInTree; use UR::Namespace::Command::Sys; use UR::Namespace::Command::Sys::ClassBrowser; use UR::Namespace::Command::Test; use UR::Namespace::Command::Test::Callcount; use UR::Namespace::Command::Test::Callcount::List; use UR::Namespace::Command::Test::Compile; use UR::Namespace::Command::Test::Eval; use UR::Namespace::Command::Test::Run; use UR::Namespace::Command::Test::TrackObjectRelease; use UR::Namespace::Command::Test::Use; use UR::Namespace::Command::Test::Window; use UR::Namespace::Command::Update; use UR::Namespace::Command::Update::ClassDiagram; use UR::Namespace::Command::Update::ClassesFromDb; use UR::Namespace::Command::Update::Pod; use UR::Namespace::Command::Update::RenameClass; use UR::Namespace::Command::Update::RewriteClassHeader; use UR::Namespace::Command::Update::SchemaDiagram; use UR::Namespace::Command::Update::TabCompletionSpec; use UR::Object; use UR::Object::Accessorized; use UR::Object::Command::FetchAndDo; use UR::Object::Command::List; use UR::Object::Command::List::Style; use UR::Object::Ghost; use UR::Object::Index; use UR::Object::Iterator; use UR::Object::Property; use UR::Object::Property::View::Default::Text; use UR::Object::Property::View::DescriptionLineItem::Text; use UR::Object::Property::View::ReferenceDescription::Text; use UR::Object::Set; use UR::Object::Set::View::Default::Json; use UR::Object::Tag; use UR::Object::Type; use UR::Object::Type::AccessorWriter; use UR::Object::Type::AccessorWriter::Product; use UR::Object::Type::AccessorWriter::Sum; use UR::Object::Type::Initializer; use UR::Object::Type::InternalAPI; use UR::Object::Type::ModuleWriter; use UR::Object::Type::View::Default::Text; use UR::Object::Value; use UR::Object::View; use UR::Object::View::Aspect; use UR::Object::View::Default::Gtk; use UR::Object::View::Default::Gtk2; use UR::Object::View::Default::Json; use UR::Object::View::Default::Text; use UR::Object::View::Lister::Text; use UR::Object::View::Toolkit; use UR::Object::View::Toolkit::Text; use UR::ObjectDeprecated; use UR::ObjectV001removed; use UR::ObjectV04removed; use UR::Observer; use UR::DBI::Report; use UR::Service::RPC::Executer; use UR::Service::RPC::Message; use UR::Service::RPC::Server; use UR::Service::RPC::TcpConnectionListener; use UR::Singleton; use UR::Test; use UR::Util; use UR::Value; use UR::Value::ARRAY; use UR::Value::Blob; use UR::Value::CSV; use UR::Value::DateTime; use UR::Value::Decimal; use UR::Value::DirectoryPath; use UR::Value::FilePath; use UR::Value::FilesystemPath; use UR::Value::FOF; use UR::Value::HASH; use UR::Value::Integer; use UR::Value::Iterator; use UR::Value::Number; use UR::Value::PerlReference; use UR::Value::SCALAR; use UR::Value::Set; use UR::Value::Text; use UR::Value::URL; use UR::Vocabulary; # optional elements if (eval "use Net::HTTPServer") { my $rv = eval "UR::Namespace::View::SchemaBrowser::CgiApp;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Base;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Class;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::File;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Index;" && eval "use UR::Namespace::View::SchemaBrowser::CgiApp::Schema;" && eval "use UR::Service::JsonRpcServer;"; die $@ unless ($rv); } if (eval "use Xml::LibXSLT") { my $rv = eval "use UR::Object::View::Default::Html;" && eval "use UR::Object::View::Default::Xsl;" && eval "use UR::Object::Set::View::Default::Xml;" && eval "use UR::Object::View::Default::Xml;" && eval "use UR::Object::Type::View::Default::Xml;" ; die $@ unless ($rv); } 1; __END__ =pod =head1 NAME UR::All =head1 SYNOPSIS use UR::All; =head1 DESCRIPTION This module exists to let software preload everything in the distribution It is slower than "use UR", but is good for things like FastCGI servers. =cut ModuleBuild.pm000444023532023421 423712121654174 15466 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::ModuleBuild; use strict; use warnings; use base 'Module::Build'; sub ACTION_clean { # FIXME: is this safe? use File::Path qw/rmtree/; rmtree "./_build"; rmtree "./blib"; unlink "./Build"; unlink "./MYMETA.yml"; } our $ns = 'UR'; our $cmd_class = 'UR::Namespace::Command'; sub ACTION_ur_docs { # We want to use UR to autodocument our code. This is done # with module introspection and requires some namespace hackery # to work. ./Build doc comes after ./Build and copies the root # namespace module into ./blib to fake a Genome namespace so this will work. use File::Copy qw/copy/; $ENV{ANSI_COLORS_DISABLED} = 1; eval { my $oldpwd = $ENV{PWD}; unshift @INC, "$ENV{PWD}/blib/lib"; my ($namespace_src_dr) = grep { -s "$_/$ns.pm" } @INC; unless ($namespace_src_dr) { die "Failed to find $ns.pm in \@INC.\n"; } chdir "$ENV{PWD}/blib/lib/$ns" || die "Can't find $ns/"; unless (-e "../$ns.pm") { copy "$namespace_src_dr/$ns.pm", "../$ns.pm" || die "Can't find $ns.pm"; } eval "use $ns"; $cmd_class->class(); UR::Namespace::Command::Update::Pod->execute( base_commands => [ $cmd_class ], ); # We need to move back for perl install to find ./lib chdir $oldpwd; }; die "failed to extract pod: $!: $@" if ($@); } sub ACTION_docs { my $self = shift; $self->depends_on('ur_docs'); $self->depends_on('code'); $self->depends_on('manpages', 'html'); } print "@UR::ModuleBuild::ISA\n"; 1; __END__ =pod =head1 NAME UR::ModuleBuild - a Module::Build subclass with UR extensions =head1 VERSION This document describes UR::ModuleBuild version 0.41. =head1 SYNOPOSIS In your Build.PL: use UR::ModuleBuild; my $build = UR::ModuleBuild->new( module_name => 'MyApp', license => 'perl', dist_version => '0.01', dist_abstract => 'my app rocks because I get to focus on the problem, not the crud', build_requires => { 'UR' => '0.32', }, requires => { 'Text::CSV_XS' => '', 'Statistics::Distributions' => '', }, ); $build->create_build_script; DataSource.pod000444023532023421 2242612121654174 15501 0ustar00abrummetgsc000000000000UR-0.41/lib/UR=pod =head1 NAME UR::DataSource - manage the the relationship between objects and a specific storage system =head1 SYNOPSIS package MyApp::DataSource::DB; class MyApp::DataSource::DB { is => ['UR::DataSource::Oracle','UR::Singleton'], }; sub server { 'main_db_server' } sub login { 'db_user' } sub auth { 'db_passwd' } sub owner { 'db_owner' } 1; =head1 DESCRIPTION Data source instances represent a logical souce of data to the application. Most of them are likely to be some kind of relational database, but not all are. UR::DataSource is an abstract base class inherited by other data sources. In normal use, your data sources will probably inherit from an abstract data source class such as L or L, as well as L. This makes it easy to link classes to this data source, since the class name will be the same as its ID, and the module autoloader will instantiate it automatically. =head1 INHERITANCE L =head1 Methods User applications will seldom interact with data sources directly. =over 4 =item autogenerate_new_object_id_for_class_name_and_rule my $id = $datasource->autogenerate_new_object_id_for_class_name_and_rule($class,$boolexpr); L calls this when the application calls create() on a class to create a new instance, but does not specify a value for the ID property. The default implementation throws an exception with C, but L is able to query a sequence in the database to generate unique IDs. A developer implementing a new data source will need to override this method and provide a sensible implementation. =item next_dummy_autogenerated_id my $int = $datasource->next_dummy_autogenerated_id() In a testing situation, is often preferable to avoid using the database's sequence for ID autogeneration but still make ID values that are unique. L calls this method if the L (see below) flag is true. The IDs generated by this method are unique during the life of the process. In addition, objects with dummy-generated IDs will never be saved to a real data source during UR::Context::commit(). =item use_dummy_autogenerated_ids $bool = $datasource->use_dummy_autogenerated_ids(); $datasource->use_dummy_autogenerated_ids($bool); Get or set a flag controlling how object IDs are autogenerated. Data source child classes should look at the value of this flag inside their implementation of C. If true, they should call C and return that value instead of attempting to generate an ID on their own. This flag is also tied to the UR_USE_DUMMY_AUTOGENERATED_IDS environment variable. =item resolve_data_sources_for_rule $possibly_other_data_source = $data_source->resolve_data_sources_for_rule($boolexpr); When L is determining which data source to use to process a get() request, it looks at the class metadata for its data source, and then calls C to give that data source a chance to defer to another data source. =item create_iterator_closure_for_rule_template_and_values $subref = $datasource->create_iterator_closure_for_rule_template_and_values( $boolexpr_tmpl, @values ); A front-end for the more widely used L =item create_iterator_closure_for_rule $subref = $datasource->create_iterator_closure_for_rule($boolexpr); This is the main entry point L uses to get data from its underlying data sources. There is no default implementation; each subclass implementing specific data source types must supply its own code. The method must accept a L $boolexpr (rule), and return a subref. Each time the subref is called it must return one arrayref of data satisfying the rule, and undef when there is no more data to return. =item _sync_database $bool = $datasource->_sync_database(changed_objects => $listref); Called by L commit(). $listref will contain all the changed objects that should be saved to that data source. The default implementation prints a warning message and returns true without saving anything. L has a functioning _sync_database() capable of generating SQL to update, insert and delete rows from the database's tables. The data source should return true if all the changes were successfully made, false if there were problems. =item commit $bool = $datasource->commit() Called by L commit(). After all data sources return true from _sync_database(), C must make those changes permanent. For RDBMS-type data sources, this commits the transaction. Return true if the commit is successful, false otherwise. =item rollback $bool = $datasource->rollback() Called by L if any data sources has problems during _sync_database or commit. It is also called by L. Data sources should reverse any changes applied during a prior C<_sync_database> that has not been made permanent by C. =item get_default_handle $scalar = $datasource->get_default_handle(); Should return the "handle" associated with any underlying logical data. For an RDBMS data source, this is the L database handle. For a file-based data source, this is the file handle. =item create_from_inline_class_data $datasource = $data_source_class_name->create_from_inline_class_data( $class_data_hashref, $datasource_data_hashref ); Called by the class initializer when a class definition contains an in-line data source definition. See L. =item _ignore_table $bool = $datasource->_ignore_table($table_name); Used to indicate whether the C command should create a class for the named table or not. If _ignore_table() returns true, then it will not create a class. =back =head1 Internal API Methods =over 4 =item _get_class_data_for_loading =item _generate_class_data_for_loading $hashref = $datasource->_resolve_query_plan($class_meta); These two methods are called by L as part of the object loading process. C<_generate_class_data_for_loading> collects information about a class and its metadata, such as property names, subclassing information and tables connected to the class, and stores that data inside the class's metadata object. C<_get_class_data_for_loading> is the main entry point; it calls C<_generate_class_data_for_loading> if the data has not been generated and cached yet, and caches the data in the class metadata object. =item _resolve_query_plan =item _generate_template_data_for_loading $hashref = $datasource->_resolve_query_plan($boolexpr_tmpl); These two methods are called by L as part of the object loading process. C<_generate_template_data_for_loading> collects information from the L $boolexpr_tmpl (rule template) and returns a hashref used later by the data source. This hashref includes hints about what classes will be involved in loading the resulting data, how those classes are joined together and how columns in the underlying query against the data source will map to properties of the class. C<_resolve_query_plan> is the main entry point; it calls C<_generate_template_data_for_loading> if the data has not been generated and cached yet, and caches the data in the rule template object. =item _generate_loading_templates_arrayref my $listref = $datasource->_generate_loading_templates_arrayref($listref); Called by _generate_template_data_for_loading(). The input is a listref of listrefs about properties involved in a query. The second-level data is sets of quads: =over =item 1. The class object for this property =item 2. The property metadata object =item 3. The database table name the data will come from =item 4 The "object number", starting with 0. This is used in inheritance or delegation where a table join will be required. =back It returns a listref of hashrefs, one hashref for every class involved in the request; usually just 1, but can be more than one if inheritance or delegation is involved. The data includes information about the class's properties, ID properties, and which columns of the result set the values will be found. =back =head1 MetaDB Each Namespace created through C will have a data source called the MetaDB. For example, the MyApp namespace's MetaDB is called MyApp::DataSource::Meta. The MetaDB is used to store information about the schemas of other data sources in the database. UR itself has a MetaDB with information about the MetaDB's schema, called L. =head1 SEE ALSO =over 4 =item L The base class for relational database Data Sources, such as L, L, L and L =item L, The base class for comma/tab delimited files =item L The base class for file multiplexor data sources. =back L, L Context.pm000444023532023421 46525012121654174 14753 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Context; use strict; use warnings; use Sub::Name; use Scalar::Util; require UR; our $VERSION = "0.41"; # UR $VERSION; use UR::Context::ImportIterator; use UR::Context::ObjectFabricator; use UR::Context::LoadingIterator; UR::Object::Type->define( class_name => 'UR::Context', is_abstract => 1, has => [ parent => { is => 'UR::Context', id_by => 'parent_id', is_optional => 1 }, query_underlying_context => { is => 'Boolean', is_optional => 1, default_value => undef, doc => 'Flag indicating whether the context must (1), must not (0) or may (undef) query underlying contexts when handling a query' }, ], doc => <get($root_id); unless ($UR::Context::root) { die "Failed to find root context object '$root_id':!? Odd value in environment variable UR_CONTEXT_ROOT?"; } if (my $base_id = $ENV{UR_CONTEXT_BASE}) { $UR::Context::base = UR::Context::Process->get($base_id); unless ($UR::Context::base) { die "Failed to find base context object '$base_id':!? Odd value in environment variable UR_CONTEXT_BASE?"; } } else { $UR::Context::base = $UR::Context::root; } $UR::Context::process = UR::Context::Process->_create_for_current_process(parent_id => $UR::Context::base->id); if (exists $ENV{'UR_CONTEXT_CACHE_SIZE_LOWWATER'} || exists $ENV{'UR_CONTEXT_CACHE_SIZE_HIGHWATER'}) { $UR::Context::destroy_should_clean_up_all_objects_loaded = 1; $cache_size_highwater = $ENV{'UR_CONTEXT_CACHE_SIZE_HIGHWATER'} || 0; $cache_size_lowwater = $ENV{'UR_CONTEXT_CACHE_SIZE_LOWWATER'} || 0; } # This changes when we initiate in-memory transactions on-top of the basic, heavier weight one for the process. $UR::Context::current = $UR::Context::process; if (exists $ENV{'UR_CONTEXT_MONITOR_QUERY'}) { $UR::Context::current->monitor_query($ENV{'UR_CONTEXT_MONITOR_QUERY'}); } $initialized = 1; return $UR::Context::current; } # the current context is either the process context, or the current transaction on-top of it *get_current = \¤t; sub current { return $UR::Context::current; } sub process { return $UR::Context::process; } sub date_template { return q|%Y-%m-%d %H:%M:%S|; } sub now { return Date::Format::time2str(date_template(), time()); } my $master_monitor_query = 0; sub monitor_query { return if $UR::Object::Type::bootstrapping; my $self = shift; $self = $UR::Context::current unless (ref $self); if (@_) { if (ref $self) { $self->{'monitor_query'} = shift; } else { $master_monitor_query = shift; } } return ref($self) ? $self->{'monitor_query'} : $master_monitor_query; } my %_query_log_times; my $query_logging_fh = IO::Handle->new(); $query_logging_fh->fdopen(fileno(STDERR), 'w'); $query_logging_fh->autoflush(1); sub query_logging_fh { $query_logging_fh = $_[1] if @_ > 1; return $query_logging_fh; } sub _log_query_for_rule { return if $UR::Object::Type::bootstrapping; my $self = shift; my($subject_class,$rule,$message) = @_; my $monitor_level; return unless ($monitor_level = $self->monitor_query); return if (substr($subject_class, 0,4) eq 'UR::' and $monitor_level < 2); # Don't log queries for internal classes my $elapsed_time = 0; if (defined($rule)) { my $time_now = Time::HiRes::time(); if (! exists $_query_log_times{$rule->id}) { $_query_log_times{$rule->id} = $time_now; } else { $elapsed_time = $time_now - $_query_log_times{$rule->id}; } } if ($elapsed_time) { $message .= sprintf(" Elapsed %.4f s", $elapsed_time); } $query_logging_fh->print($message."\n"); } sub _log_done_elapsed_time_for_rule { my($self, $rule) = @_; delete $_query_log_times{$rule->id}; } sub resolve_data_sources_for_class_meta_and_rule { my $self = shift; my $class_meta = shift; my $boolexpr = shift; ## ignored in the default case my $class_name = $class_meta->class_name; # These are some hard-coded cases for splitting up class-classes # and data dictionary entities into namespace-specific meta DBs. # Maybe there's some more generic way to move this somewhere else # FIXME This part is commented out for the moment. When class info is in the # Meta DBs, then try getting this to work #if ($class_name eq 'UR::Object::Type') { # my %params = $boolexpr->legacy_params_hash; # my($namespace) = ($params->{'class_name'} =~ m/^(\w+?)::/); # $namespace ||= $params->{'class_name'}; # In case the class name is just the namespace # # return $namespace . '::DataSource::Meta'; #} my $data_source; # For data dictionary items # When the FileMux datasource is more generalized and works for # any kind of underlying datasource, this code can move from here # and into the base class for Meta datasources if ($class_name->isa('UR::DataSource::RDBMS::Entity')) { my $params = $boolexpr->legacy_params_hash; my $namespace; if ($params->{'namespace'}) { $namespace = $params->{'namespace'}; $data_source = $params->{'namespace'} . '::DataSource::Meta'; } elsif ($params->{'data_source'} && ! ref($params->{'data_source'}) && $params->{'data_source'}->can('get_namespace')) { $namespace = $params->{'data_source'}->get_namespace; $data_source = $namespace . '::DataSource::Meta'; } elsif ($params->{'data_source'} && ref($params->{'data_source'}) eq 'ARRAY') { my %namespaces = map { $_->get_namespace => 1 } @{$params->{'data_source'}}; unless (scalar(keys %namespaces) == 1) { Carp::confess("get() across multiple namespaces is not supported"); } $namespace = $params->{'data_source'}->[0]->get_namespace; $data_source = $namespace . '::DataSource::Meta'; } else { Carp::confess("Required parameter (namespace or data_source_id) missing"); #$data_source = 'UR::DataSource::Meta'; } if (my $exists = UR::Object::Type->get($data_source)) { # switch the terminology above to stop using $data_source for the class name # now it's the object.. $data_source = $data_source->get(); } else { $self->warning_message("no data source $data_source: generating for $namespace..."); UR::DataSource::Meta->generate_for_namespace($namespace); $data_source = $data_source->get(); } unless ($data_source) { Carp::confess "Failed to find or generate a data source for meta data for namespace $namespace!"; } } else { $data_source = $class_meta->data_source; } if ($data_source) { $data_source = $data_source->resolve_data_sources_for_rule($boolexpr); } return $data_source; } # this is used to determine which data source an object should be saved-to sub resolve_data_source_for_object { my $self = shift; my $object = shift; my $class_meta = $object->__meta__; my $class_name = $class_meta->class_name; if ($class_name->isa('UR::DataSource::RDBMS::Entity') || $class_name->isa('UR::DataSource::RDBMS::Entity::Ghost')) { my $data_source = $object->data_source; my($namespace) = ($data_source =~ m/(^\w+?)::DataSource/); unless ($namespace) { Carp::croak("Can't resolve data source for object of type $class_name: The object's namespace could not be inferred from its data_source $data_source"); } my $ds_name = $namespace . '::DataSource::Meta'; return $ds_name->get(); } # Default behavior my $ds = $class_meta->data_source; return $ds; } # this turns on and off light caching (weak refs) sub _light_cache { if (@_ > 1) { $UR::Context::light_cache = $_[1]; $UR::Context::destroy_should_clean_up_all_objects_loaded = $UR::Context::light_cache; } return $UR::Context::light_cache; } # Given a rule, and a property name not mentioned in the rule, # can we infer the value of that property from what actually is in the rule? sub infer_property_value_from_rule { my($self,$wanted_property_name,$rule) = @_; # First, the easy case... The property is directly mentioned in the rule if ($rule->specifies_value_for($wanted_property_name)) { return $rule->value_for($wanted_property_name); } my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = UR::Object::Type->get($subject_class_name); my $wanted_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_name); unless ($wanted_property_meta) { $self->error_message("Class $subject_class_name has no property named $wanted_property_name"); return; } if ($wanted_property_meta->is_delegated) { $self->context_return($self->_infer_delegated_property_from_rule($wanted_property_name,$rule)); } else { $self->context_return($self->_infer_direct_property_from_rule($wanted_property_name,$rule)); } } # These are things that are changes to the program state, but not changes to the object instance # so they shouldn't be counted in the object's change_count my %changes_not_counted = map { $_ => 1 } qw(load define unload query connect); sub add_change_to_transaction_log { my ($self,$subject, $property, @data) = @_; my ($class,$id); if (ref($subject)) { $class = ref($subject); $id = $subject->id; unless ($changes_not_counted{$property} ) { $subject->{_change_count}++; #print "changing $subject $property @data\n"; } } else { $class = $subject; $subject = undef; $id = undef; } if ($UR::Context::Transaction::log_all_changes) { # eventually all calls to __signal_change__ will go directly here UR::Context::Transaction->log_change($subject, $class, $id, $property, @data); } if (my $index_list = $UR::Object::Index::all_by_class_name_and_property_name{$class}{$property}) { unless ($property eq 'create' or $property eq 'load' or $property eq 'define') { for my $index (@$index_list) { $index->_remove_object( $subject, { $property => $data[0] } ) } } unless ($property eq 'delete' or $property eq 'unload') { for my $index (@$index_list) { $index->_add_object($subject) } } } } our $sig_depth = 0; my %subscription_classes; sub send_notification_to_observers { my ($self,$subject, $property, @data) = @_; my ($class,$id); if (ref($subject)) { $class = ref($subject); $id = $subject->id; } else { $class = $subject; } my $check_classes = $subscription_classes{$class}; unless ($check_classes) { $subscription_classes{$class} = $check_classes = [ $class ? ( $class, (grep { $_->isa("UR::Object") } $class->inheritance), '' ) : ('') ]; } my @check_properties = ($property ? ($property, '') : ('') ); my @check_ids = (defined($id) ? ($id, '') : ('') ); my @matches = map { @$_ } grep { defined $_ } map { defined($id) ? @$_{@check_ids} : values(%$_) } grep { defined $_ } map { @$_{@check_properties} } grep { defined $_ } @$UR::Context::all_change_subscriptions{@$check_classes}; return unless @matches; $sig_depth++; if (@matches > 1) { no warnings; @matches = sort { $a->[2] <=> $b->[2] } @matches; }; foreach my $callback_info (@matches) { my ($callback, $note) = @$callback_info; $callback->($subject, $property, @data); } $sig_depth--; return scalar(@matches); } sub query { my $self = shift; # Fast optimization for the default case. if ( ( !ref($self) or ! $self->query_underlying_context) and ! Scalar::Util::blessed($_[1]) # This happens when query() is called with a class name and boolexpr ) { no warnings; if (exists $UR::Context::all_objects_loaded->{$_[0]}) { my $is_monitor_query = $self->monitor_query; if (my $obj = $UR::Context::all_objects_loaded->{$_[0]}->{$_[1]}) { # Matched the class and ID directly - pull it right out of the cache if ($is_monitor_query) { $self->_log_query_for_rule($_[0], undef, Carp::shortmess("QUERY: class $_[0] by ID $_[1]")); $self->_log_query_for_rule($_[0], undef, "QUERY: matched 1 cached object\nQUERY: returning 1 object\n\n"); } $obj->{'__get_serial'} = $UR::Context::GET_COUNTER++; return $obj; } elsif (my $subclasses = $UR::Object::Type::_init_subclasses_loaded{$_[0]}) { # Check subclasses of the requested class, along with the ID # yes, it only goes one level deep. This should catch enough cases to be worth it. # Deeper searches will be covered by get_objects_for_class_and_rule() foreach my $subclass (@$subclasses) { if (exists $UR::Context::all_objects_loaded->{$subclass} and my $obj = $UR::Context::all_objects_loaded->{$subclass}->{$_[1]} ) { if ($is_monitor_query) { $self->_log_query_for_rule($_[0], undef, Carp::shortmess("QUERY: class $_[0] by ID $_[1]")); $self->_log_query_for_rule($_[0], undef, "QUERY: matched 1 cached object in subclass $subclass\nQUERY: returning 1 object\n\n"); } $obj->{'__get_serial'} = $UR::Context::GET_COUNTER++; return $obj; } } } } }; # Normal logic for finding objects smartly is below. my $class = shift; # Handle the case in which this is called as an object method. # Functionality is completely different. if(ref($class)) { my @rvals; foreach my $prop (@_) { push(@rvals, $class->$prop()); } if(wantarray) { return @rvals; } else { return \@rvals; } } my ($rule, @extra) = UR::BoolExpr->resolve($class,@_); if (@extra) { # remove this and have the developer go to the datasource if (scalar @extra == 2 and ($extra[0] eq "sql" or $extra[0] eq 'sql in')) { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } # keep this part: let the sub-class handle special params if it can return $class->get_with_special_parameters($rule, @extra); } # This is here for bootstrapping reasons: we must be able to load class singletons # in order to have metadata for regular loading.... # UR::DataSource::QueryPlan isa UR::Value (which has custom loading logic), but we need to be able to generate # a QueryPlan independant of the normal loading process, otherwise there'd be endless recursion (Can't generate a QueryPlan # for a QueryPlan without generating a QueryPlan first....) if (!$rule->has_meta_options and ($class->isa("UR::Object::Type") or $class->isa("UR::Singleton") or $class->isa("UR::DataSource::QueryPlan"))) { my $normalized_rule = $rule->normalize; my @objects = $class->_load($normalized_rule); return unless defined wantarray; return @objects if wantarray; if ( @objects > 1 and defined(wantarray)) { Carp::croak("Multiple matches for $class query called in scalar context. $rule matches " . scalar(@objects). " objects"); } return $objects[0]; } return $UR::Context::current->get_objects_for_class_and_rule($class, $rule); } sub _resolve_id_for_class_and_rule { my ($self,$class_meta,$rule) = @_; my $class = $class_meta->class_name; my $id; my @id_property_names = $class_meta->id_property_names or Carp::confess( # Bad should be at least one "No id property names for class ($class). This should not have happened." ); if ( @id_property_names == 1 ) { # only 1 - try to auto generate $id = $class_meta->autogenerate_new_object_id($rule); unless ( defined $id ) { $class->error_message("Failed to auto-generate an ID for single ID property class ($class)"); return; } } else { # multiple # Try to give a useful message by getting id prop names that are not deinfed my @missed_names; for my $name ( @id_property_names ) { push @missed_names, $name unless $rule->specifies_value_for($name); } if ( @missed_names ) { # Ok - prob w/ class def, list the ones we missed $class->error_message("Attempt to create $class with multiple ids without these properties: ".join(', ', @missed_names)); return; } else { # Bad - something is really wrong... Carp::confess("Attempt to create $class failed to resolve id from underlying id properties."); } } return $id; } our $construction_method = 'create'; # Pulled out the complicated code of create_entity() below that deals with # abstract classes and subclassify_by sub _create_entity_from_abstract_class { my $self = shift; my $class = shift; my $class_meta = $class->__meta__; my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, @_); # If we can easily determine the correct subclass, delegate to that subclass' create() my $subclassify_by = $class_meta->subclassify_by(); unless (defined $subclassify_by) { Carp::croak("Can't call $construction_method on abstract class $class without a subclassify_by property"); } my $sub_class_name = $rule->value_for($subclassify_by); unless (defined $sub_class_name) { # The subclassification wasn't included in the rule my $property_meta = $class_meta->property($subclassify_by); unless ($property_meta) { Carp::croak("Abstract class $class has subclassify_by $subclassify_by, but no property exists by that name"); } # There are a few different ways the property can supply a value for subclassify_by... # The sure-fire way to get a value is to go ahead an instantiate the object into the # base/abstract class, and then we can just call the property as a method. There's # a lot of overhead in that, so first we'll try some of the easier, common-case ways if ($property_meta->default_value) { # The property has a default value $sub_class_name = $property_meta->default_value(); } elsif ($property_meta->is_calculated and ref($property_meta->calculate) eq 'CODE') { # It's calculated via a coderef my $calculate_from = $property_meta->calculate_from; my @calculate_params; foreach my $prop_name ( @$calculate_from ) { # The things in calculate_from must appear in the rule unless ($rule->specifies_value_for($prop_name)) { Carp::croak("Class $class subclassify_by calculation property '$subclassify_by' " . "requires '$prop_name' in the $construction_method() params\n" . "Params were: " . UR::Util->display_string_for_params_list($rule->params_list)); } push @calculate_params, $rule->value_for($prop_name); } my $sub = $property_meta->calculate; unless ($sub) { Carp::croak("Can't use undefined value as subroutine reference while resolving " . "value for class $class calculated property '$subclassify_by'"); } $sub_class_name = $sub->(@calculate_params); } elsif ($property_meta->is_calculated and !ref($property_meta->calculate)) { # It's calculated via a string that's eval-ed Carp::croak("Can't use a non-coderef as a calculation for class $class subclassify_by"); } elsif ($property_meta->is_delegated) { #Carp::croak("Delegated properties are not supported for subclassifying $class with property '$subclassify_by'"); my @values = $self->infer_property_value_from_rule($subclassify_by, $rule); if (! @values ) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Couldn't infer a value for indirect property '$subclassify_by' via rule $rule"); } elsif (@values > 1) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Infering a value for property '$subclassify_by' via rule $rule returned multiple values: " . join(', ', @values)); } else { $sub_class_name = $values[0]; } } else { Carp::croak("Can't use undefined value as a subclass name for $class property '$subclassify_by'"); } } unless (defined $sub_class_name) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Can't use undefined value as a subclass name for param '$subclassify_by'"); } if ($sub_class_name eq $class) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Value for $subclassify_by cannot be the same as the original class"); } unless ($sub_class_name->isa($class)) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Class $sub_class_name is not a subclass of $class"); } return $sub_class_name->$construction_method(@_); } my %memos; my %memos2; sub create_entity { my $self = shift; my $class = shift; my $memo = $memos{$class}; unless ($memo) { # we only want to grab the data necessary for object construction once # this occurs the first time a new object is created for a given class my $class_meta = $class->__meta__; my @inheritance = reverse ($class_meta, $class_meta->ancestry_class_metas); # %property_objects maps property names to UR::Object::Property objects # by going through the reversed list of UR::Object::Type objects below # We set up this hash to have the correct property objects for each property # name. This is important in the case of property name overlap via # inheritance. The property object used should be the one "closest" # to the class. In other words, a property directly on the class gets # used instead of an inherited one. my %property_objects; my %direct_properties; my %indirect_properties; my %set_properties; my %default_values; my %default_value_requires_query; my %immutable_properties; my @deep_copy_default_values; for my $co ( @inheritance ) { # Reverse map the ID into property values. # This has to occur for all subclasses which represent table rows. # deal with %property_objects my @property_objects = $co->direct_property_metas; my @property_names = map { $_->property_name } @property_objects; @property_objects{@property_names} = @property_objects; foreach my $prop ( @property_objects ) { my $name = $prop->property_name; unless (defined $name) { Carp::confess("no name on property for class " . $co->class_name . "?\n" . Data::Dumper::Dumper($prop)); } my $default_value = $prop->default_value; if (defined $default_value) { if ($prop->data_type and $prop->_data_type_as_class_name eq $prop->data_type and $prop->_data_type_as_class_name->can("get")) { # an ID or other query params in hash/array form return an object or objects $default_value_requires_query{$name} = $default_value; } elsif (ref($default_value)) { #warn ( # "a reference value $default_value is used as a default on " # . $co->class_name # . " forcing a copy during construction " # . " of $class $name..." #); push @deep_copy_default_values, $name; } $default_values{$name} = $default_value; } if ($prop->is_many) { $set_properties{$name} = $prop; } elsif ($prop->is_delegated) { $indirect_properties{$name} = $prop; } else { $direct_properties{$name} = $prop; } unless ($prop->is_mutable) { $immutable_properties{$name} = 1; } } } my @indirect_property_names = keys %indirect_properties; my @direct_property_names = keys %direct_properties; my @subclassify_by_methods; foreach my $co ( @inheritance ) { # If this class inherits from something with subclassify_by, make sure the param # actually matches. If it's not supplied, then set it to the same as the class create() # is being called on if ( $class ne $co->class_name and $co->is_abstract and my $method = $co->subclassify_by ) { push @subclassify_by_methods, $method; } } $memos{$class} = $memo = [ $class_meta, $class_meta->first_sub_classification_method_name, $class_meta->is_abstract, \@inheritance, \%property_objects, \%direct_properties, \%indirect_properties, \%set_properties, \%immutable_properties, \@subclassify_by_methods, \%default_values, (@deep_copy_default_values ? \@deep_copy_default_values : undef), \%default_value_requires_query, ]; } my ( $class_meta, $first_sub_classification_method_name, $is_abstract, $inheritance, $property_objects, $direct_properties, $indirect_properties, $set_properties, $immutable_properties, $subclassify_by_methods, $initial_default_values, $deep_copy_default_values, $default_value_requires_query, ) = @$memo; # The old way of automagic subclassing... # The class specifies that we should call a class method (sub_classification_method_name) # to determine the correct subclass if ($first_sub_classification_method_name) { my $sub_class_name = $class->$first_sub_classification_method_name(@_); if (defined($sub_class_name) and ($sub_class_name ne $class)) { # delegate to the sub-class to create the object unless ($sub_class_name->can($construction_method)) { Carp::croak("Can't locate object method '$construction_method' via package '$sub_class_name' " . "while resolving proper subclass for $class during $construction_method"); } return $sub_class_name->$construction_method(@_); } # fall through if the class names match } if ($is_abstract) { # The new way of automagic subclassing. The class specifies a property (subclassify_by) # that holds/returns the correct subclass name return $self->_create_entity_from_abstract_class($class, @_); } # normal case: make a rule out of the passed-in params # rather than normalizing the rule, we just do the extension part which is fast my $rule = UR::BoolExpr->resolve($class, @_); my $template = $rule->template; my $params = { $rule->_params_list, $template->extend_params_list_for_values(@{$rule->{values}}) }; if (my $a = $template->{_ambiguous_keys}) { my $p = $template->{_ambiguous_property_names}; @$params{@$p} = delete @$params{@$a}; } my $id = $params->{id}; unless (defined $id) { $id = $self->_resolve_id_for_class_and_rule($class_meta,$rule); unless ($id) { return; } $rule = UR::BoolExpr->resolve_normalized($class, %$params, id => $id); $params = { $rule->params_list }; ; } # handle postprocessing default values my %default_values = %$initial_default_values; #for my $name (qw//) { for my $name (keys %$default_value_requires_query) { my @id_by; if (my $id_by = $property_objects->{$name}->id_by) { @id_by = (ref($id_by) ? @$id_by : ($id_by)); } if ($params->{$name}) { delete $default_values{$name}; } elsif (@$params{@id_by}) { # some or all of the id is present # don't fall back to the default for my $id_by (@id_by) { delete $default_values{$id_by} if exists $params->{$id_by}; } delete $default_values{$name}; } else { $DB::single = 1; my $query = $default_value_requires_query->{$name}; my @query; if (ref($query) eq 'HASH') { # queries come in as a hash @query = %$query; } else { # an ID or a boolean expression @query = ($query); } my $prop = $property_objects->{$name}; my $class = $prop->_data_type_as_class_name; eval { if ($prop->is_many) { $default_values{$name} = [ $class->get(@query) ]; } else { $default_values{$name} = $class->get(@query); } }; if ($@) { warn "error setting " . $prop->class_name . " " . $prop->property_name . " to default_value from query $query for type $class!"; }; } } if ($deep_copy_default_values) { for my $name (@$deep_copy_default_values) { if ($params->{$name}) { delete $default_values{$name}; } else { $default_values{$name} = UR::Util::deep_copy($default_values{$name}); } } } # @extra is extra values gotten by inheritance my @extra; my $indirect_values = {}; for my $property_name (keys %$indirect_properties) { # pull indirect values out of the constructor hash # so we can apply them separately after making the object if ( exists $params->{ $property_name } ) { $indirect_values->{ $property_name } = delete $params->{ $property_name }; delete $default_values{$property_name}; } elsif (exists $default_values{$property_name}) { $indirect_values->{ $property_name } = delete $default_values{$property_name}; } } # if the indirect property is immutable, but it is via something which is # mutable, we use those values to get or create the bridge. my %indirect_immutable_properties_via; for my $property_name (keys %$indirect_values) { if ($immutable_properties->{$property_name}) { my $meta = $indirect_properties->{$property_name}; next unless $meta; # not indirect my $via = $meta->via; next unless $via; # not a via/to (id_by or reverse_id_by) $indirect_immutable_properties_via{$via}{$property_name} = delete $indirect_values->{$property_name}; } } for my $via (keys %indirect_immutable_properties_via) { my $via_property_meta = $class_meta->property_meta_for_name($via); my ($source_indirect_property, $source_value) = each %{$indirect_immutable_properties_via{$via}}; # There'll only ever be one key/value unless ($via_property_meta) { Carp::croak("No metadata for class $class property $via while resolving indirect value for property $source_indirect_property"); } my $indirect_property_meta = $class_meta->property_meta_for_name($source_indirect_property); unless ($indirect_property_meta) { Carp::croak("No metadata for class $class property $source_indirect_property while resolving indirect value for property $source_indirect_property"); } unless ($indirect_property_meta->to) { # We're probably dealing with a subclassify_by property where the subclass has # implicitly overridden the indirect property in the parent class with a constant-value # property in the subclass. Try asking the parent class about a property of the same name ($indirect_property_meta) = grep { $_->property_name eq $indirect_property_meta->property_name } $class_meta->ancestry_property_metas(); unless ($indirect_property_meta and $indirect_property_meta->to) { Carp::croak("Can't resolve indirect relationship for possibly overridden property '$source_indirect_property'" . " in class $class. Parent classes have no property named '$source_indirect_property'"); } } my $foreign_class = $via_property_meta->data_type; my $foreign_property = $indirect_property_meta->to; my $foreign_object = $foreign_class->get($foreign_property => $source_value); unless ($foreign_object) { # This will trigger recursion back here (into create_entity() ) if this property is multiply # indirect, such as through a bridge object $foreign_object = $foreign_class->create($foreign_property => $source_value); unless ($foreign_object) { Carp::croak("Can't create object of class $foreign_class with params ($foreign_property => '$source_value')" . " while resolving indirect value for class $class property $source_indirect_property"); } } my @joins = $indirect_property_meta->_resolve_join_chain(); my %local_properties_to_set; foreach my $join ( @joins ) { if ($join->{foreign_class}->isa("UR::Value")) { # this final "join" is to the set of values available to the raw primitive type # ...not what we really mean by delegation next; } for (my $i = 0; $i < @{$join->{'source_property_names'}}; $i++) { my $source_property_name = $join->{'source_property_names'}->[$i]; next unless (exists $direct_properties->{$source_property_name}); my $foreign_property_name = $join->{'foreign_property_names'}->[$i]; my $value = $foreign_object->$foreign_property_name; if ($rule->specifies_value_for($source_property_name) and $rule->value_for($source_property_name) ne $value) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Conflicting values for property '$source_property_name'. $construction_method rule " . "specifies value '" . $rule->value_for($source_property_name) . "' but " . "indirect immutable property '$source_indirect_property' with value " . "$source_value requires it to be '$value'"); } $local_properties_to_set{$source_property_name} = $value; } } # transfer the values we resolved back into %$params my @param_keys = keys %local_properties_to_set; @$params{@param_keys} = @local_properties_to_set{@param_keys}; } my $set_values = {}; for my $property_name (keys %$set_properties) { if (exists $params->{ $property_name }) { delete $default_values{ $property_name }; $set_values->{ $property_name } = delete $params->{ $property_name }; } } my $entity = $self->_construct_object($class, %default_values, %$params, @extra); return unless $entity; # If a property is calculated + immutable, and it wasn't supplied in the params, # that means we need to run the calculation once and store the value in the # object as a read-only attribute foreach my $property_name ( keys %$immutable_properties ) { my $property_meta = $property_objects->{$property_name}; if (!exists($params->{$property_name}) and $property_meta and $property_meta->is_calculated) { my $value = $entity->$property_name; $params->{$property_name} = $value; } } for my $subclassify_by (@$subclassify_by_methods) { my $param_value = $rule->value_for($subclassify_by); $param_value = eval { $entity->$subclassify_by } unless (defined $param_value); $param_value = $default_values{$subclassify_by} unless (defined $param_value); if (! defined $param_value) { # This should have been taken care of by the time we got here... Carp::croak("Invalid parameters for $class->$construction_method(): " . "Can't use an undefined value as a subclass name for param '$subclassify_by'"); } elsif ($param_value ne $class) { Carp::croak("Invalid parameters for $class->$construction_method(): " . "Value for subclassifying param '$subclassify_by' " . "($param_value) does not match the class it was called on ($class)"); } } # add items for any multi properties if (%$set_values) { for my $property_name (keys %$set_values) { my $meta = $set_properties->{$property_name}; my $singular_name = $meta->singular_name; my $adder = 'add_' . $singular_name; my $value = $set_values->{$property_name}; unless (ref($value) eq 'ARRAY') { die "odd non-array reference used for 'has-many' property $property_name for $class: $value!"; } for my $item (@$value) { if (ref($item) eq 'ARRAY') { $entity->$adder(@$item); } elsif (ref($item) eq 'HASH') { $entity->$adder(%$item); } else { $entity->$adder($item); } } } } # set any indirect mutable properties if (%$indirect_values) { for my $property_name (keys %$indirect_values) { $entity->$property_name($indirect_values->{$property_name}); } } if (%$immutable_properties) { my @problems = $entity->__errors__(); if (@problems) { my @errors_fatal_to_construction; my %problems_by_property_name; for my $problem (@problems) { my @problem_properties; for my $name ($problem->properties) { if ($immutable_properties->{$name}) { push @problem_properties, $name; } } if (@problem_properties) { push @errors_fatal_to_construction, join(" and ", @problem_properties) . ': ' . $problem->desc; } } if (@errors_fatal_to_construction) { my $msg = 'Failed to $construction_method ' . $class . ' with invalid immutable properties:' . join("\n", @errors_fatal_to_construction); } } } $entity->__signal_change__($construction_method); $entity->__signal_change__('load') if $construction_method eq '__define__'; $entity->{'__get_serial'} = $UR::Context::GET_COUNTER++; $UR::Context::all_objects_cache_size++; return $entity; } sub _construct_object { my $self = shift; my $class = shift; my $params = { @_ }; my $id = $params->{id}; unless (defined($id)) { Carp::confess( "No ID specified (or incomplete id params) for $class _construct_object. Params were:\n" . Data::Dumper::Dumper($params) ); } if ($UR::Context::all_objects_loaded->{$class}->{$id}) { # The object exists. This is not an exception for some reason? # We just return false to indicate that the object is not creatable. $class->error_message("An object of class $class already exists with id value '$id'"); return; } my $object; if ($object = $UR::DeletedRef::all_objects_deleted->{$class}->{$id}) { UR::DeletedRef->resurrect($object); %$object = %$params; } else { $object = bless $params, $class; } if (my $ghost = $UR::Context::all_objects_loaded->{$class . "::Ghost"}->{$id}) { # we're making something which was previously deleted and is pending save. # we must capture the old db_committed data to ensure eventual saving is done correctly. # note this object's database state in the new object so saves occurr correctly, # as an update instead of an insert. if (my $committed_data = $ghost->{db_committed}) { $object->{db_committed} = { %$committed_data }; } if (my $unsaved_data = $ghost->{'db_saved_uncommitted'}) { $object->{'db_saved_uncommitted'} = { %$unsaved_data }; } $ghost->__signal_change__("delete"); $self->_abandon_object($ghost); } # put the object in the master repository of objects for the application. $UR::Context::all_objects_loaded->{$class}{$id} = $object; # If we're using a light cache, weaken the reference. if ($UR::Context::light_cache) { # and substr($class,0,5) ne 'App::') { Scalar::Util::weaken($UR::Context::all_objects_loaded->{$class}->{$id}); } return $object; } sub delete_entity { my ($self,$entity) = @_; if (ref($entity)) { # Delete the specified object. if ($entity->{db_committed} || $entity->{db_saved_uncommitted}) { # gather params for the ghost object my $do_data_source; my %ghost_params; #my @pn; #{ no warnings 'syntax'; # @pn = grep { $_ ne 'data_source_id' || ($do_data_source=1 and 0) } # yes this really is '=' and not '==' # grep { exists $entity->{$_} } # $entity->__meta__->all_property_names; #} my(@prop_names, @many_prop_names); foreach my $prop_name ( $entity->__meta__->all_property_names) { next unless exists $entity->{$prop_name}; # skip non-directly-stored properties if ($prop_name eq 'data_source_id') { $do_data_source = 1; next; } if (ref($entity->{$prop_name}) eq 'ARRAY') { push @many_prop_names, $prop_name; } else { push @prop_names, $prop_name; } } # we're not really allowed to interrogate the data_source property directly @ghost_params{@prop_names} = $entity->get(@prop_names); # hrm doesn't work for is_many properties :( foreach my $prop_name ( @many_prop_names ) { my @values = $entity->get($prop_name); $ghost_params{$prop_name} = \@values; } if ($do_data_source) { $ghost_params{'data_source_id'} = $entity->{'data_source_id'}; } # create ghost object my $ghost = $self->_construct_object($entity->ghost_class, id => $entity->id, %ghost_params); unless ($ghost) { Carp::confess("Failed to constructe a deletion record for an unsync'd delete."); } $ghost->__signal_change__("create"); for my $com (qw(db_committed db_saved_uncommitted)) { $ghost->{$com} = $entity->{$com} if $entity->{$com}; } } $entity->__signal_change__('delete'); $self->_abandon_object($entity); return $entity; } else { Carp::confess("Can't call delete as a class method."); } } sub _abandon_object { my $self = shift; my $object = $_[0]; my $class = $object->class; my $id = $object->id; if ($object->{'__get_serial'}) { # Keep a correct accounting of objects. This one is getting deleted by a method # other than UR::Context::prune_object_cache $UR::Context::all_objects_cache_size--; } # Remove the object from the main hash. delete $UR::Context::all_objects_loaded->{$class}->{$id}; delete $UR::Context::all_objects_are_loaded->{$class}; # Remove all of the load info it is using so it'll get re-loaded if asked for later if ($object->{'__load'}) { while (my ($template_id, $rules) = each %{ $object->{'__load'}} ) { foreach my $rule_id ( keys %$rules ) { delete $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; foreach my $fabricator ( UR::Context::ObjectFabricator->all_object_fabricators ) { $fabricator->delete_from_all_params_loaded($template_id, $rule_id); } } } } # Turn our $object reference into a UR::DeletedRef. # Further attempts to use it will result in readable errors. # The object can be resurrected. if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DELETE object $object class ",$object->class," id ",$object->id,"\n"; } UR::DeletedRef->bury($object); return $object; } # This one works when the rule specifies the value of an indirect property, and we want # the value of a direct property of the class sub _infer_direct_property_from_rule { my($self,$wanted_property_name,$rule) = @_; my $rule_template = $rule->template; my @properties_in_rule = $rule_template->_property_names; # FIXME - why is this method private? my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = $subject_class_name->__meta__; my($alternate_class,$alternate_get_property, $alternate_wanted_property); my @r_values; # There may be multiple properties in the rule that will get to the wanted property PROPERTY_IN_RULE: foreach my $property_name ( @properties_in_rule) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); my $final_property_meta = $property_meta->final_property_meta || $property_meta; $alternate_get_property = $final_property_meta->property_name; $alternate_class = $final_property_meta->class_name; unless ($alternate_wanted_property) { # Either this was also a direct property of the rule, or there's no # obvious link between the indirect property and the wanted property. # the caller probably just should have done a get() $alternate_wanted_property = $wanted_property_name; $alternate_get_property = $property_name; $alternate_class = $subject_class_name; } my $value_from_rule = $rule->value_for($property_name); my @alternate_values; eval { # Inside an eval in case the get() throws an exception, the next # property in the rule may succeed my @alternate_objects = $self->query($alternate_class, $alternate_get_property => $value_from_rule ); @alternate_values = map { $_->$alternate_wanted_property } @alternate_objects; }; next unless (@alternate_values); push @r_values, \@alternate_values; } if (@r_values == 0) { # no solutions found return; } elsif (@r_values == 1) { # there was only one solution return @{$r_values[0]}; } else { # multiple solutions. Only return the intersection of them all # FIXME - this totally won't work for properties that return objects, listrefs or hashrefs # FIXME - this only works for AND rules - for now, that's all that exist my %intersection = map { $_ => 1 } @{ shift @r_values }; foreach my $list ( @r_values ) { %intersection = map { $_ => 1 } grep { $intersection{$_} } @$list; } return keys %intersection; } } # we want the value of a delegated property, and the rule specifies # a direct value sub _infer_delegated_property_from_rule { my($self, $wanted_property_name, $rule) = @_; my $rule_template = $rule->template; my $subject_class_name = $rule->subject_class_name; my $subject_class_meta = $subject_class_name->__meta__; my $wanted_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_name); unless ($wanted_property_meta->via) { Carp::croak("There is no linking meta-property (via) on property $wanted_property_name on $subject_class_name"); } my $linking_property_meta = $subject_class_meta->property_meta_for_name($wanted_property_meta->via); my $final_property_meta = $wanted_property_meta->final_property_meta; if ($linking_property_meta->reverse_as) { eval{ $linking_property_meta->data_type->class() }; # Load the class if it isn't already loaded if ($linking_property_meta->data_type ne $final_property_meta->class_name) { Carp::croak("UR::Context::_infer_delegated_property_from_rule() doesn't handle multiple levels of indiretion yet"); } } my @rule_translation = $linking_property_meta->get_property_name_pairs_for_join(); my %alternate_get_params; foreach my $pair ( @rule_translation ) { my $rule_param = $pair->[0]; next unless ($rule_template->specifies_value_for($rule_param)); my $alternate_param = $pair->[1]; my $value = $rule->value_for($rule_param); $alternate_get_params{$alternate_param} = $value; } my $alternate_class = $final_property_meta->class_name; my $alternate_wanted_property = $wanted_property_meta->to; my @alternate_values; eval { my @alternate_objects = $self->query($alternate_class, %alternate_get_params); @alternate_values = map { $_->$alternate_wanted_property } @alternate_objects; }; return @alternate_values; } sub object_cache_size_highwater { my $self = shift; if (@_) { my $value = shift; $cache_size_highwater = $value; if (defined $value) { if ($cache_size_lowwater and $value <= $cache_size_lowwater) { Carp::confess("Can't set the highwater mark less than or equal to the lowwater mark"); return; } $UR::Context::destroy_should_clean_up_all_objects_loaded = 1; $self->prune_object_cache(); } else { # turn it off $UR::Context::destroy_should_clean_up_all_objects_loaded = 0; } } return $cache_size_highwater; } sub object_cache_size_lowwater { my $self = shift; if (@_) { my $value = shift; $cache_size_lowwater = $value; if (defined($value) and $cache_size_highwater and $value >= $cache_size_highwater) { Carp::confess("Can't set the lowwater mark greater than or equal to the highwater mark"); return; } } return $cache_size_lowwater; } sub get_data_sources_for_loaded_classes { my $class = shift; my %data_source_for_class; foreach my $class ( keys %$UR::Context::all_objects_loaded ) { next if (substr($class,0,-6) eq '::Type'); # skip class objects next unless exists $UR::Context::all_objects_loaded->{$class . '::Type'}; my $class_meta = $UR::Context::all_objects_loaded->{$class . '::Type'}->{$class}; next unless $class_meta; next unless ($class_meta->is_uncachable()); $data_source_for_class{$class} = $class_meta->data_source_id; } return %data_source_for_class; } our $is_pruning = 0; sub prune_object_cache { my $self = shift; return if ($is_pruning); # Don't recurse into here return if (!defined($cache_size_highwater) or !defined($cache_size_lowwater)); return unless ($all_objects_cache_size > $cache_size_highwater); $is_pruning = 1; #$main::did_prune=1; my $t1; if ($ENV{'UR_DEBUG_OBJECT_RELEASE'} || $ENV{'UR_DEBUG_OBJECT_PRUNING'}) { $t1 = Time::HiRes::time(); print STDERR Carp::longmess("MEM PRUNE begin at $t1 ",scalar(localtime($t1)),"\n"); } my $index_id_sep = UR::Object::Index->__meta__->composite_id_separator() || "\t"; my %classes_to_prune; my %data_source_for_class = $self->get_data_sources_for_loaded_classes; foreach my $class ( keys %data_source_for_class) { $classes_to_prune{$class} = 0; } # NOTE: This pokes right into the object cache and futzes with Index IDs directly. # We can't get the Index objects though get() because we'd recurse right back into here my %indexes_by_class; foreach my $idx_id ( keys %{$UR::Context::all_objects_loaded->{'UR::Object::Index'}} ) { my $class = substr($idx_id, 0, index($idx_id, $index_id_sep)); next unless exists $classes_to_prune{$class}; push @{$indexes_by_class{$class}}, $UR::Context::all_objects_loaded->{'UR::Object::Index'}->{$idx_id}; } my $deleted_count = 0; my $pass = 0; # Make a guess about that the target serial number should be # This one goes 10% between the last time we pruned, and the last get serial # and increases by another 10% each attempt $cache_size_highwater = 1 if ($cache_size_highwater < 1); $cache_size_lowwater = 1 if ($cache_size_lowwater < 1); my $target_serial_increment = int(($GET_COUNTER - $cache_last_prune_serial) * $cache_size_lowwater / $cache_size_highwater ); #my $target_serial_increment = int(($GET_COUNTER - $cache_last_prune_serial) * 0.1); $target_serial_increment = 1 if ($target_serial_increment < 1); my $target_serial = $cache_last_prune_serial; CACHE_IS_TOO_BIG: while ($all_objects_cache_size > $cache_size_lowwater) { $pass++; $target_serial += $target_serial_increment; last if ($target_serial > $GET_COUNTER); foreach my $class (keys %classes_to_prune) { my $objects_for_class = $UR::Context::all_objects_loaded->{$class}; $indexes_by_class{$class} ||= []; foreach my $id ( keys ( %$objects_for_class ) ) { my $obj = $objects_for_class->{$id}; # Objects marked __strengthen__ed are never purged next if exists $obj->{'__strengthened'}; # classes with data sources get their objects pruned immediately if # they're marked weakened, or at the usual time (serial is under the # target) if not # Classes without data sources get instances purged if the serial # number is under the target _and_ they're marked weakened if ( ( $data_source_for_class{$class} and exists $obj->{'__weakened'} ) or ( exists $obj->{'__get_serial'} and $obj->{'__get_serial'} <= $target_serial and ($data_source_for_class{$class} or exists $obj->{'__weakened'}) and ( ! $obj->__changes__ or ! @{[$obj->__changes__]} ) ) ) { foreach my $index ( @{$indexes_by_class{$class}} ) { $index->weaken_reference_for_object($obj); } if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM PRUNE object $obj class $class id $id\n"; } delete $obj->{'__get_serial'}; Scalar::Util::weaken($objects_for_class->{$id}); $all_objects_cache_size--; $deleted_count++; $classes_to_prune{$class}++; } } } } $is_pruning = 0; $cache_last_prune_serial = $target_serial; if ($ENV{'UR_DEBUG_OBJECT_RELEASE'} || $ENV{'UR_DEBUG_OBJECT_PRUNING'}) { my $t2 = Time::HiRes::time(); printf("MEM PRUNE complete, $deleted_count objects marked after $pass passes in %.4f sec\n\n\n",$t2-$t1); } if ($all_objects_cache_size > $cache_size_lowwater) { Carp::carp "After several passes of pruning the object cache, there are still $all_objects_cache_size objects"; if ($ENV{'UR_DEBUG_OBJECT_PRUNING'}) { my @sorted_counts = sort { $a->[1] <=> $b->[1] } map { [ $_ => scalar(keys %{$UR::Context::all_objects_loaded->{$_}}) ] } keys %$UR::Context::all_objects_loaded; warn "Top 10 classes by object count:\n" . $self->_object_cache_pruning_report; } } return 1; } sub _object_cache_pruning_report { my $self = shift; my $max_show = shift; $max_show = 10 unless defined ($max_show); my @sorted_counts = sort { $b->[1] <=> $a->[1] } map { [ $_ => scalar(keys %{$UR::Context::all_objects_loaded->{$_}}) ] } keys %$UR::Context::all_objects_loaded; my $message = ''; for (my $i = 0; $i < 10 and $i < @sorted_counts; $i++) { my $class_name = $sorted_counts[$i]->[0]; my $count = $sorted_counts[$i]->[1]; $message .= "$class_name: $count\n"; if ($ENV{'UR_DEBUG_OBJECT_PRUNING'} > 1) { # more detailed info my $no_data_source = 0; my $other_references = 0; my $strengthened = 0; my $has_changes = 0; my $prunable = 0; my $class_data_source = eval { $class_name->__meta__->data_source_id; }; foreach my $obj ( values %{$UR::Context::all_objects_loaded->{$class_name}} ) { next unless $obj; my $is_prunable = 1; if (! $class_data_source ) { $no_data_source++; $is_prunable = 0; } if (! exists $obj->{'__get_serial'}) { $other_references++; $is_prunable = 0; } if (exists $obj->{'__strengthened'}) { $strengthened++; $is_prunable = 0; } if ($obj->__changes__) { $has_changes++; $is_prunable = 0; } if ($is_prunable) { $prunable++; } } $message .= sprintf("\tNo data source: %d other refs: %d strengthend: %d has changes: %d prunable: %d\n", $no_data_source, $other_references, $strengthened, $has_changes, $prunable); } } return $message; } # True if the object was loaded from an underlying context and/or datasource, or if the # object has been committed to the underlying context sub object_exists_in_underlying_context { my($self, $obj) = @_; return if ($obj->{'__defined'}); return (exists($obj->{'db_committed'}) || exists($obj->{'db_saved_uncommitted'})); } # Holds the logic for handling OR-type rules passed to get_objects_for_class_and_rule() sub _get_objects_for_class_and_or_rule { my ($self, $class, $rule, $load, $return_closure) = @_; $rule = $rule->normalize; my @u = $rule->underlying_rules; my @results; for my $u (@u) { if (wantarray or not defined wantarray) { push @results, $self->get_objects_for_class_and_rule($class,$u,$load,$return_closure); } else { my $result = $self->get_objects_for_class_and_rule($class,$u,$load,$return_closure); push @results, $result; } } if ($return_closure) { my $object_sorter = $rule->template->sorter(); my @next; return sub { # fill in missing slots in @next for(my $i = 0; $i < @results; $i++) { unless (defined $next[$i]) { # This slot got used last time through $next[$i] = $results[$i]->(); unless (defined $next[$i]) { # That iterator is exhausted, splice it out splice(@results, $i, 1); splice(@next, $i, 1); redo if $i < @results; #the next iterator is now at $i, not $i++ } } } my $lowest_slot = 0; for(my $i = 1; $i < @results; $i++) { my $cmp = $object_sorter->($next[$lowest_slot], $next[$i]); if ($cmp > 0) { $lowest_slot = $i; } elsif ($cmp == 0) { # duplicate object, mark this slot to fill in next time around $next[$i] = undef; } } my $retval = $next[$lowest_slot]; $next[$lowest_slot] = undef; return $retval; }; } # remove duplicates my $last = 0; my $plast = 0; my $next = 0; @results = grep { $plast = $last; $last = $_; $plast == $_ ? () : ($_) } sort @results; return unless defined wantarray; return @results if wantarray; if (@results > 1) { $self->_exception_for_multi_objects_in_scalar_context($rule,\@results); } return $results[0]; } # this is the underlying method for get/load/is_loaded in ::Object sub get_objects_for_class_and_rule { my ($self, $class, $rule, $load, $return_closure) = @_; my $initial_load = $load; #my @params = $rule->params_list; #print "GET: $class @params\n"; my $rule_template = $rule->template; my $group_by = $rule_template->group_by; if (ref($self) and !defined($load)) { $load = $self->query_underlying_context; # could still be undef... } if ($group_by and $rule_template->order_by) { my %group_by = map { $_ => 1 } @{ $rule->template->group_by }; foreach my $order_by_property ( @{ $rule->template->order_by } ) { unless ($group_by{$order_by_property}) { Carp::croak("Property '$order_by_property' in the -order_by list must appear in the -group_by list for BoolExpr $rule"); } } } if ( $cache_size_highwater and $all_objects_cache_size > $cache_size_highwater ) { $self->prune_object_cache(); } if ($rule_template->isa("UR::BoolExpr::Template::Or")) { return $self->_get_objects_for_class_and_or_rule($class,$rule,$load,$return_closure); } # an identifier for all objects gotten in this request will be set/updated on each of them for pruning later my $this_get_serial = $GET_COUNTER++; my $meta = $class->__meta__(); # A query on a subclass where the parent class is_abstract and has a subclassify_by property # (meaning that the parent class has a property which directly stores the proper subclass for # each row - subclasses inherit the property from the parent, and the subclass isn't is_abstract) # should have a filter added to the rule to keep only rows of the subclass we're interested in. # This will improve the SQL performance when it's later constructed. my $subclassify_by = $meta->subclassify_by; if ($subclassify_by and ! $meta->is_abstract and ! $rule->template->group_by and ! $rule->specifies_value_for($subclassify_by) ) { $rule = $rule->add_filter($subclassify_by => $class); } # If $load is undefined, and there is no underlying context, we define it to FALSE explicitly # TODO: instead of checking for a data source, skip this # We'll always go to the underlying context, even if it has nothing. # This optimization only works by coincidence since we don't stack contexts currently beyond 1. my $ds; if (!defined($load) or $load) { ($ds) = $self->resolve_data_sources_for_class_meta_and_rule($meta,$rule); if (! $ds or $class =~ m/::Ghost$/) { # Classes without data sources and Ghosts can only ever come from the cache $load = 0; } } # this is an arrayref of all of the cached data # it is set in one of two places below my $cached; # this will turn foo=>$foo into foo.id=>$foo->id where possible my $no_hard_refs_rule = $rule->flatten_hard_refs; # we do not currently fully "flatten" b/c the bx constant_values do not flatten/reframe #my $flat_rule = ( (1 or $no_hard_refs_rule->subject_class_name eq 'UR::Object::Property') ? $no_hard_refs_rule : $no_hard_refs_rule->flatten); # this is a no-op if the rule is already normalized my $normalized_rule = $no_hard_refs_rule->normalize; my $is_monitor_query = $self->monitor_query; $self->_log_query_for_rule($class,$normalized_rule,Carp::shortmess("QUERY: Query start for rule $normalized_rule")) if ($is_monitor_query); # see if we need to load if load was not defined unless (defined $load) { # check to see if the cache is complete # also returns a list of the complete cached objects where that list is found as a side-effect my ($cache_is_complete, $cached) = $self->_cache_is_complete_for_class_and_normalized_rule($class, $normalized_rule); $load = ($cache_is_complete ? 0 : 1); } if ($ds and $load and $rule_template->order_by) { # if any of the order_by is calculated, then we need to do an unordered query against the # data source, then we can do it as a non-load query and do the sorting on all the in-memory # objects my $qp = $ds->_resolve_query_plan($rule_template); if ($qp->order_by_non_column_data) { $self->_log_query_for_rule($class,$normalized_rule,"QUERY: Doing an unordered query on the datasource because one of the order_by properties of the rule is not expressable by the data source") if ($is_monitor_query); $self->get_objects_for_class_and_rule($class, $rule->remove_filter('-order')->remove_filter('-order_by'), 1); $load = 0; } } my $normalized_rule_template = $normalized_rule->template; # optimization for the common case if (!$load and !$return_closure) { my @c = $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule); my $obj_count = scalar(@c); foreach ( @c ) { unless (exists $_->{'__get_serial'}) { # This is a weakened reference. Convert it back to a regular ref my $class = ref $_; my $id = $_->id; my $ref = $UR::Context::all_objects_loaded->{$class}->{$id}; $UR::Context::all_objects_loaded->{$class}->{$id} = $ref; } $_->{'__get_serial'} = $this_get_serial; } if ($is_monitor_query) { $self->_log_query_for_rule($class,$normalized_rule,"QUERY: matched $obj_count cached objects (no loading)"); $self->_log_query_for_rule($class,$normalized_rule,"QUERY: Query complete after returning $obj_count object(s) for rule $rule"); $self->_log_done_elapsed_time_for_rule($normalized_rule); } if (defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset(\@c,$normalized_rule_template); } return @c if wantarray; # array context return unless defined wantarray; # null context Carp::confess("multiple objects found for a call in scalar context!" . Data::Dumper::Dumper(\@c)) if @c > 1; return $c[0]; # scalar context } my $object_sorter = $normalized_rule_template->sorter(); # the above process might have found all of the cached data required as a side-effect in which case # we have a value for this early # either way: ensure the cached data is known and sorted if ($cached) { @$cached = sort $object_sorter @$cached; } else { $cached = [ sort $object_sorter $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule) ]; } $self->_log_query_for_rule($class, $normalized_rule, "QUERY: matched ".scalar(@$cached)." cached objects") if ($is_monitor_query); foreach ( @$cached ) { unless (exists $_->{'__get_serial'}) { # This is a weakened reference. Convert it back to a regular ref my $class = ref $_; my $id = $_->id; my $ref = $UR::Context::all_objects_loaded->{$class}->{$id}; $UR::Context::all_objects_loaded->{$class}->{$id} = $ref; } $_->{'__get_serial'} = $this_get_serial; } # make a loading iterator if loading must be done for this rule my $loading_iterator; if ($load) { # this returns objects from the underlying context after importing them into the current context, # but only if they did not exist in the current context already $self->_log_query_for_rule($class, $normalized_rule, "QUERY: importing from underlying context with rule $normalized_rule") if ($is_monitor_query); $loading_iterator = UR::Context::LoadingIterator->_create($cached, $self,$normalized_rule, $ds,$this_get_serial); } if ($return_closure) { if ($load) { # return the iterator made above return $loading_iterator; } else { # make a quick iterator for the cached data if(defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset($cached,$normalized_rule_template); } return sub { return shift @$cached }; } } else { my @results; if ($loading_iterator) { # use the iterator made above my $found; while (defined($found = $loading_iterator->(1))) { push @results, $found; } } else { # just get the cached data if(defined($normalized_rule_template->limit) || defined($normalized_rule_template->offset)) { $self->_prune_obj_list_for_limit_and_offset($cached,$normalized_rule_template); } @results = @$cached; } return unless defined wantarray; return @results if wantarray; if (@results > 1) { $self->_exception_for_multi_objects_in_scalar_context($rule,\@results); } return $results[0]; } } sub _exception_for_multi_objects_in_scalar_context { my($self,$rule,$resultsref) = @_; my $message = sprintf("Multiple results unexpected for query.\n\tClass %s\n\trule params: %s\n\tGot %d results", $rule->subject_class_name, join(',', $rule->params_list), scalar(@$resultsref)); my $lastidx = $#$resultsref; if (@$resultsref > 10) { $message .= "; the first 10 are"; $lastidx = 9; } Carp::confess($message . ":\n" . Data::Dumper::Dumper([@$resultsref[0..$lastidx]])); } sub _prune_obj_list_for_limit_and_offset { my($self, $obj_list, $tmpl) = @_; my $limit = $tmpl->limit; my $offset = $tmpl->offset || 0; splice(@$obj_list, 0, $offset); $#$obj_list = ($limit-1); } sub __merge_db_data_with_existing_object { my($self, $class_name, $existing_object, $pending_db_object_data, $property_names) = @_; unless (defined $pending_db_object_data) { # This means a row in the database is missing for an object we loaded before if (defined($existing_object) and $self->object_exists_in_underlying_context($existing_object) and $existing_object->__changes__ ) { my $id = $existing_object->id; Carp::croak("$class_name ID '$id' previously existed in an underlying context, has since been deleted from that context, and the cached object now has unsavable changes.\nDump: ".Data::Dumper::Dumper($existing_object)."\n"); } else { #print "Removing object id ".$existing_object->id." because it has been removed from the database\n"; UR::Context::LoadingIterator->_remove_object_from_other_loading_iterators($existing_object); $existing_object->__signal_change__('delete'); $self->_abandon_object($existing_object); return $existing_object; } } my $expected_db_data; if (exists $existing_object->{'db_saved_uncommitted'}) { $expected_db_data = $existing_object->{'db_saved_uncommitted'}; } elsif (exists $existing_object->{'db_committed'}) { $expected_db_data = $existing_object->{'db_committed'}; } else { my $id = $existing_object->id; Carp::croak("$class_name ID '$id' has just been loaded, but it exists in the application as a new unsaved object!\nDump: " . Data::Dumper::Dumper($existing_object) . "\n"); } my $different = 0; my $conflict = undef; foreach my $property ( @$property_names ) { no warnings 'uninitialized'; # All direct properties are stored in the same-named hash key, right? next unless (exists $existing_object->{$property}); my $object_value = $existing_object->{$property}; my $db_value = $pending_db_object_data->{$property}; my $expected_db_value = $expected_db_data->{$property}; if ($object_value ne $expected_db_value) { $different++; } if ( $object_value eq $db_value # current value matches DB value or ($object_value eq $expected_db_value) # current value hasn't changed since it was loaded from the DB or ($db_value eq $expected_db_value) # DB value matches what it was when we loaded it from the DB ) { # no conflict. Check the next one next; } else { $conflict = $property; last; } } if (defined $conflict) { # conflicting change! # Since the user could be catching this exception, go ahead and update the # object's notion of what is in the database my %old_dbc = %$expected_db_data; @$expected_db_data{@$property_names} = @$pending_db_object_data{@$property_names}; my $old_value = defined($old_dbc{$conflict}) ? "'" . $old_dbc{$conflict} . "'" : '(undef)'; my $new_db_value = defined($pending_db_object_data->{$conflict}) ? "'" . $pending_db_object_data->{$conflict} . "'" : '(undef)'; my $new_obj_value = defined($existing_object->{$conflict}) ? "'" . $existing_object->{$conflict} . "'" : '(undef)'; my $obj_id = $existing_object->id; Carp::croak("\nA change has occurred in the database for $class_name property '$conflict' on object ID $obj_id from $old_value to $new_db_value.\n" . "At the same time, this application has made a change to that value to $new_obj_value.\n\n" . "The application should lock data which it will update and might be updated by other applications."); } # No conflicts. Update db_committed and db_saved_uncommitted based on the DB data %$expected_db_data = (%$expected_db_data, %$pending_db_object_data); if (! $different) { # FIXME HACK! This is to handle the case when you get an object, start a software transaction, # change something in the database for that object, reload the object (so __merge updates the value # found in the DB), then rollback the transaction. The act of updating the value here in __merge makes # a change record that gets undone when the transaction is rolled back. After the rollback, the current # value goes back to the originally loaded value, db_committed has the newly clhanged DB value, but # _change_count is 0 turning off change tracking makes it so this internal change isn't undone by rollback local $UR::Context::Transaction::log_all_changes = 0; # HACK! # The object has no local changes. Go ahead and update the current value, too foreach my $property ( @$property_names ) { no warnings 'uninitialized'; next if ($existing_object->{$property} eq $pending_db_object_data->{$property}); $existing_object->$property($pending_db_object_data->{$property}); } } # re-figure how many changes are really there my @change_count = $existing_object->__changes__; $existing_object->{'_change_count'} = scalar(@change_count); return $different; } sub _get_objects_for_class_and_sql { # this is a depracated back-door to get objects with raw sql # only use it if you know what you're doing my ($self, $class, $sql) = @_; my $meta = $class->__meta__; #my $ds = $self->resolve_data_sources_for_class_meta_and_rule($meta,$class->define_boolexpr()); my $ds = $self->resolve_data_sources_for_class_meta_and_rule($meta,UR::BoolExpr->resolve($class)); my $id_list = $ds->_resolve_ids_from_class_name_and_sql($class,$sql); return unless (defined($id_list) and @$id_list); my $rule = UR::BoolExpr->resolve_normalized($class, id => $id_list); return $self->get_objects_for_class_and_rule($class,$rule); } sub _cache_is_complete_for_class_and_normalized_rule { my ($self,$class,$normalized_rule) = @_; # TODO: convert this to use the rule object instead of going back to the legacy hash format my ($id,$params,@objects,$cache_is_complete); $params = $normalized_rule->legacy_params_hash; $id = $params->{id}; # Determine ahead of time whether we believe the object MUST be loaded if it exists. # If this is true, we will shortcut out of any action which loads or prepares for loading. # Try to resolve without loading in cases where we are sure # that doing so will return the complete results. my $id_only = $params->{_id_only}; $id_only = undef if ref($id) and ref($id) eq 'HASH'; if ($id_only) { # _id_only means that only id parameters were passed in. # Either a single id or an arrayref of ids. # Try to pull objects from the cache in either case if (ref $id) { # arrayref id # we check the immediate class and all derived # classes for any of the ids in the set. @objects = grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } ($class, $class->__meta__->subclasses_loaded); # see if we found all of the requested objects if (@objects == @$id) { # we found them all # return them all $cache_is_complete = 1; } else { # Ideally we'd filter out the ones we found, # but that gets complicated. # For now, we do it the slow way for partial matches @objects = (); } } else { # scalar id # Check for objects already loaded. no warnings; if (exists $all_objects_loaded->{$class}->{$id}) { $cache_is_complete = 1; @objects = grep { $_ } $all_objects_loaded->{$class}->{$id}; } else { # we already checked the immediate class, # so just check derived classes @objects = grep { $_ } map { $all_objects_loaded->{$_}->{$id} } $class->__meta__->subclasses_loaded; if (@objects) { $cache_is_complete = 1; } } } } elsif ($params->{_unique}) { # _unique means that this set of params could never # result in more than 1 object. # See if the 1 is in the cache # If not we have to load @objects = $self->_get_objects_for_class_and_rule_from_cache($class,$normalized_rule); if (@objects) { $cache_is_complete = 1; } } if ($cache_is_complete) { # if the $cache_is_comlete, the $cached list DEFINITELY represents all objects we need to return # we know that loading is NOT necessary because what we've found cached must be the entire set # Because we happen to have that set, we return it in addition to the boolean flag return wantarray ? (1, \@objects) : (); } # We need to do more checking to see if loading is necessary # Either the parameters were non-unique, or they were unique # and we didn't find the object checking the cache. # See if we need to do a load(): my $template_id = $normalized_rule->template_id; my $rule_id = $normalized_rule->id; my $loading_is_in_progress_on_another_iterator = grep { $_->is_loading_in_progress_for_boolexpr($normalized_rule) } UR::Context::ObjectFabricator->all_object_fabricators; return 0 if $loading_is_in_progress_on_another_iterator; # complex (non-single-id) params my $loading_was_done_before_with_these_params = ( # exact match to previous attempt ( exists ($UR::Context::all_params_loaded->{$template_id}) and exists ($UR::Context::all_params_loaded->{$template_id}->{$rule_id}) ) || # this is a subset of a previous attempt ($self->_loading_was_done_before_with_a_superset_of_this_rule($normalized_rule)) ); my $object_is_loaded_or_non_existent = $loading_was_done_before_with_these_params || $class->all_objects_are_loaded; if ($object_is_loaded_or_non_existent) { # These same non-unique parameters were used to load previously, # or we loaded everything at some point. # No load necessary. return 1; } else { # Load according to params return; } } # done setting $load, and possibly filling $cached/$cache_is_complete as a side-effect sub all_objects_loaded { my $self = shift; my $class = $_[0]; return( grep {$_} map { values %{ $UR::Context::all_objects_loaded->{$_} } } $class, $class->__meta__->subclasses_loaded ); } sub all_objects_loaded_unsubclassed { my $self = shift; my $class = $_[0]; return (grep {$_} values %{ $UR::Context::all_objects_loaded->{$class} } ); } sub _get_objects_for_class_and_rule_from_cache { # Get all objects which are loaded in the application which match # the specified parameters. my ($self, $class, $rule) = @_; my ($template,@values) = $rule->template_and_values; #my @param_list = $rule->params_list; #print "CACHE-GET: $class @param_list\n"; my $strategy = $rule->{_context_query_strategy}; unless ($strategy) { if ($rule->template->group_by) { $strategy = $rule->{_context_query_strategy} = "set intersection"; } elsif ($rule->num_values == 0) { $strategy = $rule->{_context_query_strategy} = "all"; } elsif ($rule->is_id_only) { $strategy = $rule->{_context_query_strategy} = "id"; } else { $strategy = $rule->{_context_query_strategy} = "index"; } } my @results = eval { if ($strategy eq "all") { return $self->all_objects_loaded($class); } elsif ($strategy eq "id") { my $id = $rule->value_for_id(); unless (defined $id) { $id = $rule->value_for_id(); } # Try to get the object(s) from this class directly with the ID. # Note that the code below is longer than it needs to be, but # is written to run quickly by resolving the most common cases # first, and gathering data only if and when it must. my @matches; if (ref($id) eq 'ARRAY') { # The $id is an arrayref. Get all of the set. @matches = grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } ($class); # We're done if the number found matches the number of ID values. return @matches if @matches == @$id; } else { # The $id is a normal scalar. if (not defined $id) { #Carp::carp("Undefined id passed as params for query on $class"); Carp::cluck("\n\n**** Undefined id passed as params for query on $class"); $id ||= ''; } my $match; # FIXME This is a performance optimization for class metadata to avoid the search through # @subclasses_loaded a few lines further down. When 100s of classes are loaded it gets # a bit slow. Maybe UR::Object::Type should override get() instad and put it there? if (! $UR::Object::Type::bootstrapping and $class eq 'UR::Object::Type') { my $meta_class_name = $id . '::Type'; $match = $all_objects_loaded->{$meta_class_name}->{$id} || $all_objects_loaded->{'UR::Object::Type'}->{$id}; if ($match) { return $match; } else { return; } } $match = $all_objects_loaded->{$class}->{$id}; # We're done if we found anything. If not we keep checking. return $match if $match; } # Try to get the object(s) from this class's subclasses. # We may be adding to matches made above is we used an arrayref # and the results are incomplete. my @subclasses_loaded = $class->__meta__->subclasses_loaded; return @matches unless @subclasses_loaded; if (ref($id) eq 'ARRAY') { # The $id is an arrayref. Get all of the set and add it to anything found above. push @matches, grep { $_ } map { @$_{@$id} } map { $all_objects_loaded->{$_} } @subclasses_loaded; } else { # The $id is a normal scalar, but we didn't find it above. # Try each subclass, exiting if we find anything. for (@subclasses_loaded) { my $match = $all_objects_loaded->{$_}->{$id}; return $match if $match; } } # Since an ID was specified, and we've scanned the core hash every way possible, # we're done. Return nothing if necessary. return @matches; } elsif ($strategy eq "index") { # FIXME - optimize by using the rule (template?)'s param names directly to get the # index id instead of re-figuring it out each time my $class_meta = $rule->subject_class_name->__meta__; my %params = $rule->params_list; my $should_evaluate_later; for my $key (keys %params) { if (substr($key,0,1) eq '-' or substr($key,0,1) eq '_') { delete $params{$key}; } elsif ($key =~ /^\w*\./) { # a chain of properties $should_evaluate_later = 1; delete $params{$key}; } else { my $prop_meta = $class_meta->property_meta_for_name($key); # NOTE: We _could_ remove the is_delegated check if we knew we were operating on # a read-only context. if ($prop_meta && ($prop_meta->is_many or $prop_meta->is_delegated)) { # These indexes perform poorly in the general case if we try to index # the is_many properties. Instead, strip them out from the basic param # list, and evaluate the superset of indexed objects through the rule $should_evaluate_later = 1; delete $params{$key}; } } } my @properties = sort keys %params; unless (@properties) { # All the supplied filters were is_many properties return grep { $rule->evaluate($_) } $self->all_objects_loaded($class); } my @values = map { $params{$_} } @properties; unless (@properties == @values) { Carp::confess(); } # find or create the index my $pstring = join(",",@properties); my $index_id = UR::Object::Index->__meta__->resolve_composite_id_from_ordered_values($class,$pstring); my $index = $all_objects_loaded->{'UR::Object::Index'}{$index_id}; $index ||= UR::Object::Index->create( id => $index_id, indexed_class_name => $class, indexed_property_string => $pstring ); # add the indexed objects to the results list if ($UR::Debug::verify_indexes) { my @matches = $index->get_objects_matching(@values); @matches = sort @matches; my @matches2 = sort grep { $rule->evaluate($_) } $self->all_objects_loaded($class); unless ("@matches" eq "@matches2") { print "@matches\n"; print "@matches2\n"; #Carp::cluck("Mismatch!"); my @matches3 = $index->get_objects_matching(@values); my @matches4 = $index->get_objects_matching(@values); return @matches2; } return @matches; } if ($should_evaluate_later) { return grep { $rule->evaluate($_) } $index->get_objects_matching(@values); } else { return $index->get_objects_matching(@values); } } elsif ($strategy eq 'set intersection') { #print $rule->num_values, " ", $rule->is_id_only, "\n"; my $template = $rule->template; my $group_by = $template->group_by; # get the objects in memory, and make sets for them if they do not exist my $rule_no_group = $rule->remove_filter('-group_by'); $rule_no_group = $rule_no_group->remove_filter('-order_by'); my @objects_in_set = $self->_get_objects_for_class_and_rule_from_cache($class, $rule_no_group); my @sets_from_grouped_objects = _group_objects($rule_no_group->template,\@values,$group_by,\@objects_in_set); # determine the template that the grouped subsets will use # find templates which are subsets of that template # find sets with a my $set_class = $class . '::Set'; my $expected_template_id = $rule->template->_template_for_grouped_subsets->id; my @matches = grep { # TODO: make the template something indexable so we can pull from index my $bx = UR::BoolExpr->get($_->id); my $bxt = $bx->template; if ($bxt->id ne $expected_template_id) { #print "TEMPLATE MISMATCH $expected_template_id does not match $bxt->{id}! set: $_ with bxid $bx->{id} cannot be under rule $rule_no_group" . Data::Dumper::Dumper($_); (); } elsif (not $bx->is_subset_of($rule_no_group) ) { #print "SUBSET MISMATCH: $rule_no_group is not a superset of $_ with bxid $bx->{id}" . Data::Dumper::Dumper($_); (); } else { #print "MATCH: $rule_no_group with $expected_template_id matches $bx $bx->{id}" . Data::Dumper::Dumper($_); ($_); } } $self->all_objects_loaded($set_class); # Code to check that newly fabricated set definitions are in the set we query back out: # my @all = $self->all_objects_loaded($set_class); # my %expected; # @expected{@sets_from_grouped_objects} = @sets_from_grouped_objects; # for my $match (@matches) { # delete $expected{$match}; # } # if (keys %expected) { # #$DB::single = 1; # print Data::Dumper::Dumper(\%expected); # } return @matches; } else { die "unknown strategy $strategy"; } }; # Handle passing-through any exceptions. die $@ if $@; if (my $recurse = $template->recursion_desc) { my ($this,$prior) = @$recurse; # remove undef items. undef/NULL in the recursion linkage means it doesn't link to anything my @values = grep { defined } map { $_->$prior } @results; if (@values) { # We do get here, so that adjustments to intermediate foreign keys # in the cache will result in a new query at the correct point, # and not result in missing data. #push @results, $class->get($this => \@values, -recurse => $recurse); push @results, map { $class->get($this => $_, -recurse => $recurse) } @values; } } my $group_by = $template->group_by; #if ($group_by) { # # return sets instead of the actual objects # @results = _group_objects($template,\@values,$group_by,\@results); #} if (@results > 1) { my $sorter; if ($group_by) { # We need to rewrite the original rule on the member class to be a rule # on the Set class to do proper ordering my $set_class = $template->subject_class_name . '::Set'; my $set_template = UR::BoolExpr::Template->resolve($set_class, -group_by => $group_by); $sorter = $set_template->sorter; } else { $sorter = $template->sorter; } @results = sort $sorter @results; } # Return in the standard way. return @results if (wantarray); Carp::confess("Multiple matches for $class @_!") if (@results > 1); return $results[0]; } sub _group_objects { my ($template,$values,$group_by,$objects) = @_; my $sub_template = $template->remove_filter('-group_by'); for my $property (@$group_by) { $sub_template = $sub_template->add_filter($property); } my $set_class = $template->subject_class_name . '::Set'; my @groups; my %seen; for my $result (@$objects) { my %values_for_group_property; foreach my $group_property ( @$group_by ) { my @values = $result->$group_property; if (@values) { $values_for_group_property{$group_property} = \@values; } else { $values_for_group_property{$group_property} = [ undef ]; } } my @combinations = UR::Util::combinations_of_values(map { $values_for_group_property{$_} } @$group_by); foreach my $extra_values ( @combinations ) { my $bx = $sub_template->get_rule_for_values(@$values,@$extra_values); next if $seen{$bx->id}++; my $group = $set_class->get($bx->id); push @groups, $group; } } return @groups; } sub _loading_was_done_before_with_a_superset_of_this_rule { my($self,$rule) = @_; my $template = $rule->template; if (exists $UR::Context::all_params_loaded->{$template->id} and exists $UR::Context::all_params_loaded->{$template->id}->{$rule->id} ) { return 1; } my @rule_values = $rule->values; my @rule_param_names = $template->_property_names; my %rule_values; for (my $i = 0; $i < @rule_param_names; $i++) { $rule_values{ $rule_param_names[$i] } = $rule_values[$i]; } foreach my $loaded_template_id ( keys %$UR::Context::all_params_loaded ) { my $loaded_template = UR::BoolExpr::Template->get($loaded_template_id); if($template->is_subset_of($loaded_template)) { # Try limiting the possibilities by matching the previously-loaded rule value_id's # on this rule's values my @param_names = $loaded_template->_property_names; my @values = @rule_values{ @param_names }; my $value_id; { no warnings 'uninitialized'; $value_id = join($UR::BoolExpr::Util::record_sep, @values); } my @candidates = grep { index($_, $value_id) > 0 } keys(%{ $UR::Context::all_params_loaded->{$loaded_template_id} }); foreach my $loaded_rule_id ( @candidates ) { my $loaded_rule = UR::BoolExpr->get($loaded_rule_id); return 1 if ($rule->is_subset_of($loaded_rule)); } } } return; } sub _forget_loading_was_done_with_template_and_rule { my($self,$template_id, $rule_id) = @_; delete $all_params_loaded->{$template_id}->{$rule_id}; } # Given a list of values, returns a list of lists containing all subsets of # the input list, including the original list and the empty list sub _get_all_subsets_of_params { my $self = shift; return [] unless @_; my $first = shift; my @rest = $self->_get_all_subsets_of_params(@_); return @rest, map { [$first, @$_ ] } @rest; } sub query_underlying_context { my $self = shift; unless (ref $self) { $self = $self->current; } if (@_) { $self->{'query_underlying_context'} = shift; } return $self->{'query_underlying_context'}; } # all of these delegate to the current context... sub has_changes { return shift->get_current->has_changes(@_); } sub commit { Carp::carp 'UR::Context::commit() called as a function, not a method. Assumming commit on current context' unless @_; my $self = shift; $self = UR::Context->current() unless ref $self; $self->__signal_change__('precommit'); unless ($self->_sync_databases) { $self->__signal_change__('commit',0); return; } unless ($self->_commit_databases) { $self->__signal_change__('commit',0); die "Application failure during commit!"; } $self->__signal_change__('commit',1); foreach ( $self->all_objects_loaded('UR::Object') ) { delete $_->{'_change_count'}; } return 1; } sub rollback { my $self = shift; unless ($self) { warn 'UR::Context::rollback() called as a function, not a method. Assumming rollback on current context'; $self = UR::Context->current(); } $self->__signal_change__('prerollback'); unless ($self->_reverse_all_changes) { $self->__signal_change__('rollback', 0); die "Application failure during reverse_all_changes?!"; } unless ($self->_rollback_databases) { $self->__signal_change__('rollback', 0); die "Application failure during rollback!"; } $self->__signal_change__('rollback', 1); return 1; } sub _tmp_self { my $self = shift; if (ref($self)) { return ($self,ref($self)); } else { return ($UR::Context::current, $self); } } sub clear_cache { my ($self,$class) = _tmp_self(shift @_); my %args = @_; # dont unload any of the infrastructional classes, or any classes # the user requested to be saved my %local_dont_unload; if ($args{'dont_unload'}) { for my $class_name (@{$args{'dont_unload'}}) { $local_dont_unload{$class_name} = 1; for my $subclass_name ($class_name->__meta__->subclasses_loaded) { $local_dont_unload{$subclass_name} = 1; } } } for my $class_name (UR::Object->__meta__->subclasses_loaded) { # Once transactions are fully implemented, the command params will sit # beneath the regular transaction, so we won't need this. For now, # we need a work-around. next if $class_name eq "UR::Command::Param"; next if $class_name->isa('UR::Singleton'); my $class_obj = $class_name->__meta__; #if ($class_obj->data_source and $class_obj->is_transactional) { # # normal #} #elsif (!$class_obj->data_source and !$class_obj->is_transactional) { # # expected #} #elsif ($class_obj->data_source and !$class_obj->is_transactional) { # Carp::confess("!!!!!data source on non-transactional class $class_name?"); #} #elsif (!$class_obj->data_source and $class_obj->is_transactional) { # # okay #} next unless $class_obj->is_uncachable; next if $class_obj->is_meta_meta; next unless $class_obj->is_transactional; next if ($local_dont_unload{$class_name} || grep { $class_name->isa($_) } @{$args{'dont_unload'}}); next if $class_obj->is_meta; next if not defined $class_obj->data_source; for my $obj ($self->all_objects_loaded_unsubclassed($class_name)) { # Check the type against %local_dont_unload again, because all_objects_loaded() # will return child class objects, as well as the class you asked for. For example, # GSC::DNA->a_o_l() will also return GSC::ReadExp objects, and the user may have wanted # to save those. We also check whether the $obj type isa one of the requested classes # because, for example, GSC::Sequence->a_o_l returns GSC::ReadExp types, and the user # may have wanted to save all GSC::DNAs my $obj_type = ref $obj; next if ($local_dont_unload{$obj_type} || grep {$obj_type->isa($_) } @{$args{'dont_unload'}}); $obj->unload; } my @obj = grep { defined($_) } values %{ $UR::Context::all_objects_loaded->{$class_name} }; if (@obj) { $class->warning_message("Skipped unload of $class_name objects during clear_cache: " . join(",",map { $_->id } @obj ) . "\n" ); if (my @changed = grep { $_->__changes__ } @obj) { require YAML; $class->error_message( "The following objects have changes:\n" . Data::Dumper::Dumper(\@changed) . "The clear_cache method cannot be called with unsaved changes on objects.\n" . "Use reverse_all_changes() first to really undo everything, then clear_cache()," . " or call sync_database() and clear_cache() if you want to just lighten memory but keep your changes.\n" . "Clearing the cache with active changes will be supported after we're sure all code like this is gone. :)\n" ); exit 1; } } delete $UR::Context::all_objects_loaded->{$class_name}; delete $UR::Context::all_objects_are_loaded->{$class_name}; delete $UR::Context::all_params_loaded->{$class_name}; } 1; } our $IS_SYNCING_DATABASE = 0; sub _sync_databases { my $self = shift; my %params = @_; # Glue App::DB->sync_database with UR::Context->_sync_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API # You'll also want to remove all the gotos from this function and uncomment # the returns return 1 if $IS_SYNCING_DATABASE; $IS_SYNCING_DATABASE = 1; if ($App::DB::{'sync_database'}) { unless (App::DB->sync_database() ) { $IS_SYNCING_DATABASE = 0; $self->error_message(App::DB->error_message()); return; } } $IS_SYNCING_DATABASE = 0; # This should be far down enough to avoid recursion, right? my @o = grep { ref($_) eq 'UR::DeletedRef' } $self->all_objects_loaded('UR::Object'); if (@o) { print Data::Dumper::Dumper(\@o); Carp::confess(); } # Determine what has changed. my @changed_objects = ( $self->all_objects_loaded('UR::Object::Ghost'), grep { $_->__changes__ } $self->all_objects_loaded('UR::Object') #UR::Util->mapreduce_grep(sub { $_[0]->__changes__ },$self->all_objects_loaded('UR::Object')) ); return 1 unless (@changed_objects); # Ensure validity. # This is primarily to catch custom validity logic in class overrides. my @invalid = grep { $_->__errors__ } @changed_objects; #my @invalid = UR::Util->mapreduce_grep(sub { $_[0]->__errors__}, @changed_objects); if (@invalid) { $self->display_invalid_data_for_save(\@invalid); goto PROBLEM_SAVING; #return; } # group changed objects by data source my %ds_objects; for my $obj (@changed_objects) { my $data_source = $self->resolve_data_source_for_object($obj); next unless $data_source; my $data_source_id = $data_source->id; $ds_objects{$data_source_id} ||= { 'ds_obj' => $data_source, 'changed_objects' => []}; push @{ $ds_objects{$data_source_id}->{'changed_objects'} }, $obj; } my @ds_with_can_savepoint_and_class = map { [ $ds_objects{$_}->{'ds_obj'}->can_savepoint, $ds_objects{$_}->{'ds_obj'}->class, $_ ] } keys %ds_objects; my @ds_in_order = map { $_->[2] } sort { ( $a->[0] <=> $b->[0] ) || ( $a->[1] cmp $b->[1] ) } @ds_with_can_savepoint_and_class; # save on each in succession my @done; my $rollback_on_non_savepoint_handle; for my $data_source_id (@ds_in_order) { my $obj_list = $ds_objects{$data_source_id}->{'changed_objects'}; my $data_source = $ds_objects{$data_source_id}->{'ds_obj'}; my $result = $data_source->_sync_database( %params, changed_objects => $obj_list, ); if ($result) { push @done, $data_source; next; } else { $self->error_message( "Failed to sync data source: $data_source_id: " . $data_source->error_message ); for my $prev_data_source (@done) { $prev_data_source->_reverse_sync_database; } goto PROBLEM_SAVING; #return; } } return 1; PROBLEM_SAVING: if ($App::DB::{'rollback'}) { App::DB->rollback(); } return; } sub display_invalid_data_for_save { my $self = shift; my @objects_with_errors = @{shift @_}; $self->error_message('Invalid data for save!'); for my $obj (@objects_with_errors) { no warnings; my $msg = $obj->class . " identified by " . $obj->__display_name__ . " has problems on\n"; my @problems = $obj->__errors__; foreach my $error ( @problems ) { $msg .= $error->__display_name__ . "\n"; } $msg .= " Current state:\n"; my $datadumper = Data::Dumper::Dumper($obj); my $nr_of_lines = $datadumper =~ tr/\n//; if ($nr_of_lines > 40) { # trim it down to the first and last 15 lines $datadumper =~ m/^((?:.*\n){15})/; $msg .= $1; $datadumper =~ m/((?:.*\n?){3})$/; $msg .= "[...]\n$1\n"; } else { $msg .= $datadumper; } $self->error_message($msg); } return 1; } sub _reverse_all_changes { my $self = shift; my $class; if (ref($self)) { $class = ref($self); } else { $class = $self; $self = $UR::Context::current; } @UR::Context::Transaction::open_transaction_stack = (); @UR::Context::Transaction::change_log = (); $UR::Context::Transaction::log_all_changes = 0; $UR::Context::current = $UR::Context::process; # aggregate the objects to be deleted # this prevents cirucularity, since some objects # can seem re-reversible (like ghosts) my %_delete_objects; my @all_subclasses_loaded = sort UR::Object->__meta__->subclasses_loaded; for my $class_name (@all_subclasses_loaded) { next unless $class_name->can('__meta__'); next if $class_name->isa("UR::Value"); my @objects_this_class = $self->all_objects_loaded_unsubclassed($class_name); next unless @objects_this_class; $_delete_objects{$class_name} = \@objects_this_class; } # do the reverses for my $class_name (keys %_delete_objects) { my $co = $class_name->__meta__; next unless $co->is_transactional; my $objects_this_class = $_delete_objects{$class_name}; if ($class_name->isa("UR::Object::Ghost")) { # ghose placeholder for a deleted object for my $object (@$objects_this_class) { # revive ghost object my $ghost_copy = eval("no strict; no warnings; " . Data::Dumper::Dumper($object)); if ($@) { Carp::confess("Error re-constituting ghost object: $@"); } my($saved_data, $saved_key); if (exists $ghost_copy->{'db_saved_uncommitted'} ) { $saved_data = $ghost_copy->{'db_saved_uncommitted'}; } elsif (exists $ghost_copy->{'db_committed'} ) { $saved_data = $ghost_copy->{'db_committed'}; } else { next; # This shouldn't happen?! } my $new_object = $object->live_class->UR::Object::create( %$saved_data ); $new_object->{db_committed} = $ghost_copy->{db_committed} if (exists $ghost_copy->{'db_committed'}); $new_object->{db_saved_uncommitted} = $ghost_copy->{db_saved_uncommitted} if (exists $ghost_copy->{'db_saved_uncommitted'}); unless ($new_object) { Carp::confess("Failed to re-constitute $object!"); } next; } } else { # non-ghost regular entity # find property_names (that have columns) # todo: switch to check persist my %property_names = map { $_->property_name => $_ } grep { defined $_->column_name } map { $co->property_meta_for_name($_) } $co->all_property_names; for my $object (@$objects_this_class) { # find columns which make up the primary key # convert to a hash where property => 1 my @id_property_names = $co->all_id_property_names; my %id_props = map {($_, 1)} @id_property_names; my $saved = $object->{db_saved_uncommitted} || $object->{db_committed}; if ($saved) { # Existing object. Undo all changes since last sync, # or since load occurred when there have been no syncs. foreach my $property_name ( keys %property_names ) { # only do this if the column is not part of the # primary key my $property_meta = $property_names{$property_name}; next if ($id_props{$property_name} || $property_meta->is_delegated || $property_meta->is_legacy_eav || ! $property_meta->is_mutable || $property_meta->is_transient || $property_meta->is_constant); $object->$property_name($saved->{$property_name}); } delete $object->{'_change_count'}; } elsif ($object->isa('UR::DeletedRef')) { # DeletedRefs can appear if un-doing some items causes others in @$objects_this_class # to get deleted because of observers of their own. Skip these 1; } else { # Object not in database, get rid of it. # Because we only go back to the last sync not (not last commit), # this no longer has to worry about rolling back an uncommitted database save which may have happened. if ($object->isa('UR::Observer')) { UR::Observer::delete($object); # Observers have some state that needs to get cleaned up } else { UR::Object::delete($object); } } } # next non-ghost object } } # next class return 1; } our $IS_COMMITTING_DATABASE = 0; sub _commit_databases { my $class = shift; # Glue App::DB->commit() with UR::Context->_commit_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API return 1 if $IS_COMMITTING_DATABASE; $IS_COMMITTING_DATABASE = 1; if ($App::DB::{'commit'}) { unless (App::DB->commit() ) { $IS_COMMITTING_DATABASE = 0; $class->error_message(App::DB->error_message()); return; } } $IS_COMMITTING_DATABASE = 0; unless ($class->_for_each_data_source("commit")) { if ($class->error_message eq "PARTIAL commit") { die "FRAGMENTED DISTRIBUTED TRANSACTION\n" . Data::Dumper::Dumper($UR::Context::all_objects_loaded) } else { die "FAILED TO COMMIT!: " . $class->error_message; } } return 1; } our $IS_ROLLINGBACK_DATABASE = 0; sub _rollback_databases { my $class = shift; # Glue App::DB->rollback() with UR::Context->_rollback_databases() # and avoid endless recursion. # FIXME Remove this when we're totally off of the old API return 1 if $IS_ROLLINGBACK_DATABASE; $IS_ROLLINGBACK_DATABASE = 1; if ($App::DB::{'rollback'}) { unless (App::DB->rollback()) { $IS_ROLLINGBACK_DATABASE = 0; $class->error_message(App::DB->error_message()); return; } } $IS_ROLLINGBACK_DATABASE = 0; $class->_for_each_data_source("rollback") or die "FAILED TO ROLLBACK!: " . $class->error_message; return 1; } sub _disconnect_databases { my $class = shift; $class->_for_each_data_source("disconnect"); return 1; } sub _for_each_data_source { my($class,$method) = @_; my @ds = $UR::Context::current->all_objects_loaded('UR::DataSource'); foreach my $ds ( @ds ) { unless ($ds->$method) { $class->error_message("$method failed on DataSource ",$ds->get_name); return; } } return 1; } sub _get_committed_property_value { my $class = shift; my $object = shift; my $property_name = shift; if ($object->{'db_committed'}) { return $object->{'db_committed'}->{$property_name}; } elsif ($object->{'db_saved_uncommitted'}) { return $object->{'db_saved_uncommitted'}->{$property_name}; } else { return; } } sub _dump_change_snapshot { my $class = shift; my %params = @_; my @c = grep { $_->__changes__ } $UR::Context::current->all_objects_loaded('UR::Object'); my $fh; if (my $filename = $params{filename}) { $fh = IO::File->new(">$filename"); unless ($fh) { $class->error_message("Failed to open file $filename: $!"); return; } } else { $fh = "STDOUT"; } require YAML; $fh->print(YAML::Dump(\@c)); $fh->close; } sub reload { my $self = shift; # this is here for backward external compatability # get() now goes directly to the context my $class = shift; if (ref $class) { # Trying to reload a specific object? if (@_) { Carp::confess("load() on an instance with parameters is not supported"); return; } @_ = ('id' ,$class->id()); $class = ref $class; } my ($rule, @extra) = UR::BoolExpr->resolve_normalized($class,@_); if (@extra) { if (scalar @extra == 2 and ($extra[0] eq "sql" or $extra[0] eq 'sql in')) { return $UR::Context::current->_get_objects_for_class_and_sql($class,$extra[1]); } else { die "Odd parameters passed directly to $class load(): @extra.\n" . "Processable params were: " . Data::Dumper::Dumper({ $rule->params_list }); } } return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,1); } ## This is old, untested code that we may wany to resurrect at some point # #our $CORE_DUMP_VERSION = 1; ## Use Data::Dumper to save a representation of the object cache to a file. Args are: ## filename => the name of the file to save to ## dumpall => boolean flagging whether to dump _everything_, or just the things ## that would actually be loaded later in core_restore() # #sub _core_dump { # my $class = shift; # my %args = @_; # # my $filename = $args{'filename'} || "/tmp/core." . UR::Context::Process->prog_name . ".$ENV{HOST}.$$"; # my $dumpall = $args{'dumpall'}; # # my $fh = IO::File->new(">$filename"); # if (!$fh) { # $class->error_message("Can't open dump file $filename for writing: $!"); # return undef; # } # # my $dumper; # if ($dumpall) { # Go ahead and dump everything # $dumper = Data::Dumper->new([$CORE_DUMP_VERSION, # $UR::Context::all_objects_loaded, # $UR::Context::all_objects_are_loaded, # $UR::Context::all_params_loaded, # $UR::Context::all_change_subscriptions], # ['dump_version','all_objects_loaded','all_objects_are_loaded', # 'all_params_loaded','all_change_subscriptions']); # } else { # my %DONT_UNLOAD = # map { # my $co = $_->__meta__; # if ($co and not $co->is_transactional) { # ($_ => 1) # } # else { # () # } # } # $UR::Context::current->all_objects_loaded('UR::Object'); # # my %aol = map { ($_ => $UR::Context::all_objects_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_objects_loaded; # my %aoal = map { ($_ => $UR::Context::all_objects_are_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_objects_are_loaded; # my %apl = map { ($_ => $UR::Context::all_params_loaded->{$_}) } # grep { ! $DONT_UNLOAD{$_} } keys %$UR::Context::all_params_loaded; # # don't dump $UR::Context::all_change_subscriptions # $dumper = Data::Dumper->new([$CORE_DUMP_VERSION,\%aol, \%aoal, \%apl], # ['dump_version','all_objects_loaded','all_objects_are_loaded', # 'all_params_loaded']); # # } # # $dumper->Purity(1); # For dumping self-referential data structures # $dumper->Sortkeys(1); # Makes quick and dirty file comparisons with sum/diff work correctly-ish # # $fh->print($dumper->Dump() . "\n"); # # $fh->close; # # return $filename; #} # ## Read a file previously generated with core_dump() and repopulate the object cache. Args are: ## filename => name of the coredump file ## force => boolean flag whether to go ahead and attempt to load the file even if it thinks ## there is a formatting problem #sub _core_restore { # my $class = shift; # my %args = @_; # my $filename = $args{'filename'}; # my $forcerestore = $args{'force'}; # # my $fh = IO::File->new("$filename"); # if (!$fh) { # $class->error_message("Can't open dump file $filename for restoring: $!"); # return undef; # } # # my $code; # while (<$fh>) { $code .= $_ } # # my($dump_version,$all_objects_loaded,$all_objects_are_loaded,$all_params_loaded,$all_change_subscriptions); # eval $code; # # if ($@) # { # $class->error_message("Failed to restore core file state: $@"); # return undef; # } # if ($dump_version != $CORE_DUMP_VERSION) { # $class->error_message("core file's version $dump_version differs from expected $CORE_DUMP_VERSION"); # return 0 unless $forcerestore; # } # # my %DONT_UNLOAD = # map { # my $co = $_->__meta__; # if ($co and not $co->is_transactional) { # ($_ => 1) # } # else { # () # } # } # $UR::Context::current->all_objects_loaded('UR::Object'); # # # Go through the loaded all_objects_loaded, prune out the things that # # are in %DONT_UNLOAD # my %loaded_classes; # foreach ( keys %$all_objects_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_objects_loaded->{$_} = $all_objects_loaded->{$_}; # $loaded_classes{$_} = 1; # # } # foreach ( keys %$all_objects_are_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_objects_are_loaded->{$_} = $all_objects_are_loaded->{$_}; # $loaded_classes{$_} = 1; # } # foreach ( keys %$all_params_loaded ) { # next if ($DONT_UNLOAD{$_}); # $UR::Context::all_params_loaded->{$_} = $all_params_loaded->{$_}; # $loaded_classes{$_} = 1; # } # # $UR::Context::all_change_subscriptions is basically a bunch of coderef # # callbacks that can't reliably be dumped anyway, so we skip it # # # Now, get the classes to instantiate themselves # foreach ( keys %loaded_classes ) { # $_->class() unless m/::Ghost$/; # } # # return 1; #} 1; =pod =head1 NAME UR::Context - Manage the current state of the application =head1 SYNOPSIS use AppNamespace; my $obj = AppNamespace::SomeClass->get(id => 1234); $obj->some_property('I am changed'); UR::Context->get_current->rollback; # some_property reverts to its original value $obj->other_property('Now, I am changed'); UR::Context->commit; # other_property now permanently has that value =head1 DESCRIPTION The main application code will rarely interact with UR::Context objects directly, except for the C and C methods. It manages the mappings between an application's classes, object cache, and external data sources. =head1 SUBCLASSES UR::Context is an abstract class. When an application starts up, the system creates a handful of Contexts that logically exist within one another: =over 2 =item 1. L - A context to represent all the data reachable in the application's namespace. It connects the application to external data sources. =item 2. L - A context to represent the state of data within the currently running application. It handles the transfer of data to and from the Root context, through the object cache, on behalf of the application code. =item 3. L - A context to represent an in-memory transaction as a diff of the object cache. The Transaction keeps a list of changes to objects and is able to revert those changes with C, or apply them to the underlying context with C. =back =head1 CONSTRUCTOR =over 4 =item begin my $trans = UR::Context::Transaction->begin(); L instances are created through C. =back A L and L context will be created for you when the application initializes. Additional instances of these classes are not usually instantiated. =head1 METHODS Most of the methods below can be called as either a class or object method of UR::Context. If called as a class method, they will operate on the current context. =over 4 =item get_current my $context = UR::Context::get_current(); Returns the UR::Context instance of whatever is the most currently created Context. Can be called as a class or object method. =item query_underlying_context my $should_load = $context->query_underlying_context(); $context->query_underlying_context(1); A property of the Context that sets the default value of the C<$should_load> flag inside C as described below. Initially, its value is undef, meaning that during a get(), the Context will query the underlying data sources only if this query has not been done before. Setting this property to 0 will make the Context never query data sources, meaning that the only objects retrievable are those already in memory. Setting the property to 1 means that every query will hit the data sources, even if the query has been done before. =item get_objects_for_class_and_rule @objs = $context->get_objects_for_class_and_rule( $class_name, $boolexpr, $should_load, $should_return_iterator ); This is the method that serves as the main entry point to the Context behind the C, and C methods of L, and C method of UR::Context. C<$class_name> and C<$boolexpr> are required arguments, and specify the target class by name and the rule used to filter the objects the caller is interested in. C<$should_load> is a flag indicating whether the Context should load objects satisfying the rule from external data sources. A true value means it should always ask the relevent data sources, even if the Context believes the requested data is in the object cache, A false but defined value means the Context should not ask the data sources for new data, but only return what is currently in the cache matching the rule. The value C means the Context should use the value of its query_underlying_context property. If that is also undef, then it will use its own judgement about asking the data sources for new data, and will merge cached and external data as necessary to fulfill the request. C<$should_return_iterator> is a flag indicating whether this method should return the objects directly as a list, or iterator function instead. If true, it returns a subref that returns one object each time it is called, and undef after the last matching object: my $iter = $context->get_objects_for_class_and_rule( 'MyClass', $rule, undef, 1 ); my @objs; while (my $obj = $iter->()); push @objs, $obj; } =item has_changes my $bool = $context->has_changes(); Returns true if any objects in the given Context's object cache (or the current Context if called as a class method) have any changes that haven't been saved to the underlying context. =item commit UR::Context->commit(); Causes all objects with changes to save their changes back to the underlying context. If the current context is a L, then the changes will be applied to whatever Context the transaction is a part of. if the current context is a L context, then C pushes the changes to the underlying L context, meaning that those changes will be applied to the relevent data sources. In the usual case, where no transactions are in play and all data sources are RDBMS databases, calling C will cause the program to begin issuing SQL against the databases to update changed objects, insert rows for newly created objects, and delete rows from deleted objects as part of an SQL transaction. If all the changes apply cleanly, it will do and SQL C, or C if not. commit() returns true if all the changes have been safely transferred to the underlying context, false if there were problems. =item rollback UR::Context->rollback(); Causes all objects' changes for the current transaction to be reversed. If the current context is a L, then the transactional properties of those objects will be reverted to the values they had when the transaction started. Outside of a transaction, object properties will be reverted to their values when they were loaded from the underlying data source. rollback() will also ask all the underlying databases to rollback. =item clear_cache UR::Context->clear_cache(); Asks the current context to remove all non-infrastructional data from its object cache. This method will fail and return false if any object has changes. =item resolve_data_source_for_object my $ds = $obj->resolve_data_source_for_object(); For the given C<$obj> object, return the L instance that object was loaded from or would be saved to. If objects of that class do not have a data source, then it will return C. =item resolve_data_sources_for_class_meta_and_rule my @ds = $context->resolve_data_sources_for_class_meta_and_rule($class_obj, $boolexpr); For the given class metaobject and boolean expression (rule), return the list of data sources that will need to be queried in order to return the objects matching the rule. In most cases, only one data source will be returned. =item infer_property_value_from_rule my $value = $context->infer_property_value_from_rule($property_name, $boolexpr); For the given boolean expression (rule), and a property name not mentioned in the rule, but is a property of the class the rule is against, return the value that property must logically have. For example, if this object is the only TestClass object where C is the value 'bar', it can infer that the TestClass property C must have the value 'blah' in the current context. my $obj = TestClass->create(id => 1, foo => 'bar', baz=> 'blah'); my $rule = UR::BoolExpr->resolve('TestClass', foo => 'bar); my $val = $context->infer_property_value_from_rule('baz', $rule); # val now is 'blah' =item object_cache_size_highwater UR::Context->object_cache_size_highwater(5000); my $highwater = UR::Context->object_cache_size_highwater(); Set or get the value for the Context's object cache pruning high water mark. The object cache pruner will be run during the next C if the cache contains more than this number of prunable objects. See the L section below for more information. =item object_cache_size_lowwater UR::Context->object_cache_size_lowwater(5000); my $lowwater = UR::Context->object_cache_size_lowwater(); Set or get the value for the Context's object cache pruning high water mark. The object cache pruner will stop when the number of prunable objects falls below this number. =item prune_object_cache UR::Context->prune_object_cache(); Manually run the object cache pruner. =item reload UR::Context->reload($object); UR::Context->reload('Some::Class', 'property_name', value); Ask the context to load an object's data from an underlying Context, even if the object is already cached. With a single parameter, it will use that object's ID parameters as the basis for querying the data source. C will also accept a class name and list of key/value parameters the same as C. =item _light_cache UR::Context->_light_cache(1); Turn on or off the light caching flag. Light caching alters the behavior of the object cache in that all object references in the cache are made weak by Scalar::Util::weaken(). This means that the application code must keep hold of any object references it wants to keep alive. Light caching defaults to being off, and must be explicitly turned on with this method. =back =head1 Custom observer aspects UR::Context sends signals for observers watching for some non-standard aspects. =over 2 =item precommit After C has been called, but before any changes are saved to the data sources. The only parameters to the Observer's callback are the Context object and the aspect ("precommit"). =item commit After C has been called, and after an attempt has been made to save the changes to the data sources. The parameters to the callback are the Context object, the aspect ("commit"), and a boolean value indicating whether the commit succeeded or not. =item prerollback After C has been called, but before and object state is reverted. =item rollback After C has been called, and after an attempt has been made to revert the state of all the loaded objects. The parameters to the callback are the Context object, the aspect ("rollback"), and a boolean value indicating whether the rollback succeeded or not. =back =head1 Data Concurrency Currently, the Context is optimistic about data concurrency, meaning that it does very little to prevent clobbering data in underlying Contexts during a commit() if other processes have changed an object's data after the Context has cached and object. For example, a database has an object with ID 1 and a property with value 'bob'. A program loads this object and changes the property to 'fred', but does not yet commit(). Meanwhile, another program loads the same object, changes the value to 'joe' and does commit(). Finally the first program calls commit(). The final value in the database will be 'fred', and no exceptions will be raised. As part of the caching behavior, the Context keeps a record of what the object's state is as it's loaded from the underlying Context. This is how the Context knows what object have been changed during C. If an already cached object's data is reloaded as part of some other query, data consistency of each property will be checked. If there are no conflicting changes, then any differences between the object's initial state and the current state in the underlying Context will be applied to the object's notion of what it thinks its initial state is. In some future release, UR may support additional data concurrency methods such as pessimistic concurrency: check that the current state of all changed (or even all cached) objects in the underlying Context matches the initial state before committing changes downstream. Or allowing the object cache to operate in write-through mode for some or all classes. =head1 Internal Methods There are many methods in UR::Context meant to be used internally, but are worth documenting for anyone interested in the inner workings of the Context code. =over 4 =item _create_import_iterator_for_underlying_context $subref = $context->_create_import_iterator_for_underlying_context( $boolexpr, $data_source, $serial_number ); $next_obj = $subref->(); This method is part of the object loading process, and is called by L when it is determined that the requested data does not exist in the object cache, and data should be brought in from another, underlying Context. Usually this means the data will be loaded from an external data source. C<$boolexpr> is the L rule, usually from the application code. C<$data_source> is the L that will be used to load data from. C<$serial_number> is used by the object cache pruner. Each object loaded through this iterator will have $serial_number in its C<__get_serial> hashref key. It works by first getting an iterator for the data source (the C<$db_iterator>). It calls L to find out how data is to be loaded and whether this request spans multiple data sources. It calls L to get a list of closures to transform the primary data source's data into UR objects, and L (if necessary) to get more closures that can load and join data from the primary to the secondary data source(s). It returns a subref that works as an iterator, loading and returning objects one at a time from the underlying context into the current context. It returns undef when there are no more objects to return. The returned iterator works by first asking the C<$db_iterator> for the next row of data as a listref. Asks the secondary data source joiners whether there is any matching data. Calls the object fabricator closures to convert the data source data into UR objects. If any of the object requires subclassing, then additional importing iterators are created to handle that. Finally, the objects matching the rule are returned to the caller one at a time. =item _resolve_query_plan_for_ds_and_bxt my $query_plan = $context->_resolve_query_plan_for_ds_and_bxt( $data_source, $boolexpr_tmpl ); my($query_plan, @addl_info) = $context->_resolve_query_plan_for_ds_and_bxt( $data_source, $boolexpr_tmpl ); When a request is made that will hit one or more data sources, C<_resolve_query_plan_for_ds_and_bxt> is used to call a method of the same name on the data source. It retuns a hashref used by many other parts of the object loading system, and describes what data source to use, how to query that data source to get the objects, how to use the raw data returned by the data source to construct objects and how to resolve any delegated properties that are a part of the rule. C<$data_source> is a L object ID. C<$coolexpr_tmpl> is a L object. In the common case, the query will only use one data source, and this method returns that data directly. But if the primary data source sets the C key on the data structure as may be the case when a rule involves a delegated property to a class that uses a different data source, then this methods returns an additional list of data. For each additional data source needed to resolve the query, this list will have three items: =over 2 =item 1. The secondary data source ID =item 2. A listref of delegated L objects joining the primary data source to this secondary data source. =item 3. A L rule template applicable against the secondary data source =back =item _create_secondary_rule_from_primary my $new_rule = $context->_create_secondary_rule_from_primary( $primary_rule, $delegated_properties, $secondary_rule_tmpl ); When resolving a request that requires multiple data sources, this method is used to construct a rule against applicable against the secondary data source. C<$primary_rule> is the L rule used in the original query. C<$delegated_properties> is a listref of L objects as returned by L linking the primary to the secondary data source. C<$secondary_rule_tmpl> is the rule template, also as returned by L. =item _create_secondary_loading_closures my($obj_importers, $joiners) = $context->_create_secondary_loading_closures( $primary_rule_tmpl, @addl_info); When reolving a request that spans multiple data sources, this method is used to construct two lists of subrefs to aid in the request. C<$primary_rule_tmpl> is the L rule template made from the original rule. C<@addl_info> is the same list returned by L. For each secondary data source, there will be one item in the two listrefs that are returned, and in the same order. C<$obj_importers> is a listref of subrefs used as object importers. They transform the raw data returned by the data sources into UR objects. C<$joiners> is also a listref of subrefs. These closures know how the properties link the primary data source data to the secondary data source. They take the raw data from the primary data source, load the next row of data from the secondary data source, and returns the secondary data that successfully joins to the primary data. You can think of these closures as performing the same work as an SQL C between data in different data sources. =item _cache_is_complete_for_class_and_normalized_rule ($is_cache_complete, $objects_listref) = $context->_cache_is_complete_for_class_and_normalized_rule( $class_name, $boolexpr ); This method is part of the object loading process, and is called by L to determine if the objects requested by the L C<$boolexpr> will be found entirely in the object cache. If the answer is yes then C<$is_cache_complete> will be true. C<$objects_listef> may or may not contain objects matching the rule from the cache. If that list is not returned, then L does additional work to locate the matching objects itself via L It does its magic by looking at the C<$boolexpr> and loosely matching it against the query cache C<$UR::Context::all_params_loaded> =item _get_objects_for_class_and_rule_from_cache @objects = $context->_get_objects_for_class_and_rule_from_cache( $class_name, $boolexpr ); This method is called by L when L<_cache_is_complete_for_class_and_normalized_rule> says the requested objects do exist in the cache, but did not return those items directly. The L C<$boolexpr> contains hints about how the matching data is likely to be found. Its C<_context_query_strategy> key will contain one of three values =over 2 =item 1. all This rule is against a class with no filters, meaning it should return every member of that class. It calls C<$class-Eall_objects_loaded> to extract all objects of that class in the object cache. =item 2. id This rule is against a class and filters by only a single ID, or a list of IDs. The request is fulfilled by plucking the matching objects right out of the object cache. =item 3. The category for any other rule. This request is fulfilled by getting a previously created L for this rule, or creating a new UR::Object::Index, and calling L. =back =item _loading_was_done_before_with_a_superset_of_this_params_hashref $bool = $context->_loading_was_done_before_with_a_superset_of_this_params_hashref( $class_name, $params_hashref ); This method is used by L to determine if the requested data was asked for previously, either from a get() asking for a superset of the current request, or from a request on a parent class of the current request. For example, if a get() is done on a class with one param: @objs = ParentClass->get(param_1 => 'foo'); And then later, another request is done with an additional param: @objs2 = ParentClass->get(param_1 => 'foo', param_2 => 'bar'); Then the first request must have returned all the data that could have possibly satisfied the second request, and so the system will not issue a query against the data source. As another example, given those two previously done queries, if another get() is done on a class that inherits from ParentClass @objs3 = ChildClass->get(param_1 => 'foo'); again, the first request has already loaded all the relevent data, and therefore won't query the data source. =item _sync_databases $bool = $context->_sync_databases(); Starts the process of committing all the Context's changes to the external data sources. _sync_databases() is the workhorse behind L. First, it finds all objects with changes. Checks those changed objects for validity with C<$obj-Einvalid>. If any objects are found invalid, then _sync_databases() will fail. Finally, it bins all the changed objects by data source, and asks each data source to save those objects' changes. It returns true if all the data sources were able to save the changes, false otherwise. =item _reverse_all_changes $bool = $context->_reverse_all_changes(); _reverse_all_changes() is the workhorse behind L. For each class, it goes through each object of that class. If the object is a L, representing a deleted object, it converts the ghost back to the live version of the object. For other classes, it makes a list of properties that have changed since they were loaded (represented by the C hash key in the object), and reverts those changes by using each property's accessor method. =back =head1 The Object Cache The object cache is integral to the way the Context works, and also the main difference between UR and other ORMs. Other systems do no caching and require the calling application to hold references to any objects it is interested in. Say one part of the app loads data from the database and gives up its references, then if another part of the app does the same or similar query, it will have to ask the database again. UR handles caching of classes, objects and queries to avoid asking the data sources for data it has loaded previously. The object cache is essentially a software transaction that sits above whatever database transaction is active. After objects are loaded, any changes, creations or deletions exist only in the object cache, and are not saved to the underlying data sources until the application explicitly requests a commit or rollback. Objects are returned to the application only after they are inserted into the object cache. This means that if disconnected parts of the application are returned objects with the same class and ID, they will have references to the same exact object reference, and changes made in one part will be visible to all other parts of the app. An unchanged object can be removed from the object cache by calling its C method. Since changes to the underlying data sources are effectively delayed, it is possible that the application's notion of the object's current state does not match the data stored in the data source. You can mitigate this by using the C class or object method to fetch the latest data if it's a problem. Another issue to be aware of is if multiple programs are likely to commit conflicting changes to the same data, then whichever applies its changes last will win; some kind of external locking needs to be applied. Finally, if two programs attempt to insert data with the same ID columns into an RDBMS table, the second application's commit will fail, since that will likely violate a constraint. =head2 Object Change Tracking As objects are loaded from their data sources, their properties are initialized with the data from the query, and a copy of the same data is stored in the object in its C hash key. Anyone can ask the object for a list of its changes by calling C<$obj-Echanged>. Internally, changed() goes through all the object's properties, comparing the current values in the object's hash with the same keys under 'db_committed'. Objects created through the C class method have no 'db_committed', and so the object knows it it a newly created object in this context. Every time an object is retrieved with get() or through an iterator, it is assigned a serial number in its C<__get_serial> hash key from the C<$UR::Context::GET_SERIAL> counter. This number is unique and increases with each get(), and is used by the L to expire the least recently requested data. Objects also track what parameters have been used to get() them in the hash C<$obj-E{__load}>. This is a copy of the data in C<$UR::Context::all_params_loaded-E{$template_id}>. For each rule ID, it will have a count of the number of times that rule was used in a get(). =head2 Deleted Objects and Ghosts Calling delete() on an object is tracked in a different way. First, a new object is created, called a ghost. Ghost classes exist for every class in the application and are subclasses of L. For example, the ghost class for MyClass is MyClass::Ghost. This ghost object is initialized with the data from the original object. The original object is removed from the object cache, and is reblessed into the UR::DeletedRef class. Any attempt to interact with the object further will raise an exception. Ghost objects are not included in a get() request on the regular class, though the app can ask for them specificly using Cget(%params)>. Ghost classes do not have ghost classes themselves. Calling create() or delete() on a Ghost class or object will raise an exception. Calling other methods on the Ghost object that exist on the original, live class will delegate over to the live class's method. =head2 all_objects_are_loaded C<$UR::Context::all_objects_are_loaded> is a hashref keyed by class names. If the value is true, then L knows that all the instances of that class exist in the object cache, and it can avoid asking the underlying context/datasource for that class' data. =head2 all_params_loaded C<$UR::Context::all_params_loaded> is a two-level hashref. The first level is class names. The second level is rule (L) IDs. The values are how many times that class and rule have been involved in a get(). This data is used by L to determine if the requested data will be found in the object cache for non-id queries. =head2 all_objects_loaded C<$UR::Context::all_objects_loaded> is a two-level hashref. The first level is class names. The second level is object IDs. Every time an object is created, defined or loaded from an underlying context, it is inserted into the C hash. For queries involving only ID properties, the Context can retrieve them directly out of the cache if they appear there. The entire cache can be purged of non-infrastructional objects by calling L. =head2 Object Cache Pruner The default Context behavior is to cache all objects it knows about for the entire life of the process. For programs that churn through large amounts of data, or live for a long time, this is probably not what you want. The Context has two settings to loosely control the size of the object cache. L and L. As objects are created and loaded, a count of uncachable objects is kept in C<$UR::Context::all_objects_cache_size>. The first part of L checks to see of the current size is greater than the highwater setting, and call L if so. prune_object_cache() works by looking at what C<$UR::Context::GET_SERIAL> was the last time it ran, and what it is now, and making a guess about what object serial number to use as a guide for removing objects by starting at 10% of the difference between the last serial and the current value, called the target serial. It then starts executing a loop as long as C<$UR::Context::all_objects_cache_size> is greater than the lowwater setting. For each uncachable object, if its C<__get_serial> is less than the target serial, it is weakened from any Les it may be a member of, and then weakened from the main object cache, C<$UR::Context::all_objects_loaded>. The application may lock an object in the cache by calling C<__strengthen__> on it, Likewise, the app may hint to the pruner to throw away an object as soon as possible by calling C<__weaken__>. =head1 SEE ALSO L, L, L, L, L, L =cut DeletedRef.pm000444023532023421 710612121654174 15262 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::DeletedRef; use strict; use warnings; require UR; BEGIN { # this is to workaround a Perl bug where the overload magic flag is not updated # for references with a different RV (which happens anytime you do "my $object" # https://rt.perl.org/rt3/Public/Bug/Display.html?id=9472 if ($^V lt v5.8.9) { eval "use overload fallback => 1"; }; }; our $VERSION = "0.41"; # UR $VERSION; our $all_objects_deleted = {}; sub bury { my $class = shift; for my $object (@_) { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM BURY object $object class ",$object->class," id ",$object->id,"\n"; } my $original_class = ref($object); my $original_id = $object->id; %$object = (original_class => ref($object), original_data => {%$object}); bless $object, 'UR::DeletedRef'; $all_objects_deleted->{$original_class}->{$original_id} = $object; Scalar::Util::weaken($all_objects_deleted->{$original_class}->{$original_id}); } return 1; } sub resurrect { shift unless (ref($_[0])); foreach my $object (@_) { my $original_class = $object->{'original_class'}; bless $object, $original_class; %$object = (%{$object->{original_data}}); my $id = $object->id; delete $all_objects_deleted->{$original_class}->{$id}; $object->resurrect_object if ($object->can('resurrect_object')); } return 1; } use Data::Dumper; sub AUTOLOAD { our $AUTOLOAD; my $method = $AUTOLOAD; $method =~ s/^.*:://g; Carp::croak("Attempt to use a reference to an object which has been deleted. A call was made to method '$method'\nRessurrect it first.\n" . Dumper($_[0])); } sub DESTROY { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY deletedref $_[0]\n"; } delete $all_objects_deleted->{"$_[0]"}; } 1; =pod =head1 NAME UR::DeletedRef - Represents an instance of a no-longer-existent object =head1 SYNOPSIS my $obj = Some::Class->get(123); $obj->delete; print ref($obj),"\n"; # prints 'UR::DeletedRef' $obj->some_method(); # generates an exception through Carp::confess $obj->resurrect; print ref($obj),"\n"; # prints 'Some::Class' =head1 DESCRIPTION Object instances become UR::DeletedRefs when some part of the application calls delete() or unload() on them, meaning that they no longer exist in that Context. The extant object reference is turned into a UR::DeletedRef so that if that same reference is used in any capacity later in the program, it will generate an exception through its AUTOLOAD to prevent using it by mistake. Note that UR::DeletedRef instances are different than Ghost objects. When a UR-based object is deleted through delete(), a new Ghost object reference is created from the data in the old object, and the old object reference is re-blessed as a UR::DeletedRef. Any variables still referencing the original object now hold a reference to this UR::DeletedRef. The Ghost object can be retrieved by issuing a get() against the Ghost class. Objects unloaded from the Context using unload(), or indirectly by rolling-back a transaction which triggers unload of objects loaded during the transaction, are also turned into UR::DeletedRefs. You aren't likely to encounter UR::DeletedRefs in normal use. What usually happens is that an object will be deleted with delete() (or unload()), the lexical variable pointing to the DeletedRef will soon go out of scope and the DeletedRef will then be garbage-colelcted. =head1 SEE ALSO UR::Object, UR::Object::Ghost, UR::Context =cut Change.pm000444023532023421 1142212121654174 14460 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::Change; use strict; use warnings; use IO::File; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, has => [ changed_class_name => { is => 'String' }, changed_id => { }, changed_aspect => { is => 'String' }, undo_data => { is_optional => 1 }, # Some changes (like create) have no undo data ], is_transactional => 1, ); sub undo { my $self = shift; my $changed_class_name = $self->changed_class_name; my $changed_id = $self->changed_id; my $changed_aspect = $self->changed_aspect; my $undo_data = $self->undo_data; if (0) { no warnings; my @k = qw/changed_class_name changed_id changed_aspect undo_data/; my @v = @$self{@k}; print "\tundoing @v\n"; }; # Ghosts are managed internally by create/delete. # Allow reversal of those methods to indirectly reverse ghost changes. if ($changed_class_name =~ /::Ghost/) { if ($changed_aspect !~ /^(create|delete)(_object|)$/) { Carp::confess("Unlogged change on ghost? @_"); } return 1; } # For tracking "external" changes allow the undo to execute a closure if ($changed_aspect eq 'external_change') { if (ref($undo_data) eq 'CODE') { return eval { &$undo_data }; } else { die $self->error_message("'external_change' expects a code ref for undo data!"); } } my $changed_obj; if ($changed_aspect eq "delete" or $changed_aspect eq "unload") { $undo_data = '' unless defined $undo_data; $changed_obj = eval "no strict; no warnings; " . $undo_data; bless($changed_obj, 'UR::DeletedRef') if (ref $changed_obj); # changed class so that UR::Object::DESTROY is not called on a "fake" UR::Object if ($@) { Carp::confess("Error reconstructing $changed_aspect data for @_: $@"); } return unless $changed_obj; } else { $changed_obj = $changed_class_name->get($changed_id); } # TODO: if no changed object, die? if ($changed_aspect eq "__define__") { UR::Object::unload($changed_obj); } elsif ($changed_aspect eq "create") { if ($changed_obj->isa('UR::Observer')) { UR::Observer::delete($changed_obj); # Observers have state that needs to be cleaned up } else { UR::Object::delete($changed_obj); } } elsif ($changed_aspect eq "delete") { my %stored; for my $key (keys %$changed_obj) { if ($key =~ /^(status|warning|error|debug)_message$/ or ref($changed_obj->{$key}) ) { $stored{$key} = delete $changed_obj->{$key}; } } $changed_obj = UR::Object::create($changed_class_name,%$changed_obj); for my $key (keys %stored) { $changed_obj->{$key} = $stored{$key}; } $changed_obj->{'_change_count'}--; # it was incremented when delete() was called on the object } elsif ($changed_aspect eq "load") { UR::Object::unload($changed_obj); } elsif ($changed_aspect eq "load_external") { } elsif ($changed_aspect eq "unload") { $changed_obj = $UR::Context::current->_construct_object($changed_class_name,%$changed_obj); UR::Object::__signal_change__($changed_obj,"load") if $changed_obj; } elsif ($changed_aspect eq "commit") { if ($changed_obj->isa('UR::Context::Transaction')) { UR::Object::unload($changed_obj); } else { Carp::confess(); } } elsif ($changed_aspect eq "rollback") { Carp::confess(); } elsif ($changed_aspect eq 'rewrite_module_header') { my $VAR1; eval $undo_data; my $filename = $VAR1->{'path'}; my $data = $VAR1->{'data'}; if (defined $data) { # The file previously existed, restore the old contents my $f = IO::File->new(">$filename"); unless ($f) { Carp::confess("Can't open $filename for writing while undo on rewrite_module_header for class $changed_class_name: $!"); } $f->print($data); $f->close(); } else { # The file did not previously exist, remove the file unlink($filename); } } else { # regular property if ($changed_obj->can($changed_aspect)) { $changed_obj->$changed_aspect($undo_data); $changed_obj->{'_change_count'} -= 2; # 2 because the line above will actually increment the counter, too } } $changed_obj->{'_change_count'} = 0 if ($changed_obj->{'_change_count'} and $changed_obj->{'_change_count'} < 0); return 1; } 1; Object.pm000444023532023421 13031312121654174 14522 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Object; use warnings; use strict; require UR; use Scalar::Util; our @ISA = ('UR::ModuleBase'); our $VERSION = "0.41"; # UR $VERSION;; # Base object API sub class { ref($_[0]) || $_[0] } sub id { $_[0]->{id} } sub create { $UR::Context::current->create_entity(@_); } sub get { $UR::Context::current->query(@_); } sub delete { $UR::Context::current->delete_entity(@_); } # Meta API sub __context__ { # In UR, a "context" handles inter-object references so they can cross # process boundaries, and interact with persistance systems automatically. # For efficiency, all context switches update a package-level value. # We will ultimately need to support objects recording their context explicitly # for things such as data maintenance operations. This shouldn't happen # during "business logic". return $UR::Context::current; } sub __meta__ { # the class meta object # subclasses set this specifically for efficiency upon construction # the base class has a generic implementation for boostrapping Carp::cluck("using the default __meta__!"); my $class_name = shift; return $UR::Context::all_objects_loaded->{"UR::Object::Type"}{$class_name}; } # The identity operation. Not particularly useful by itself, but makes # things like mapping operations easier and calculate_from metadata able # to include the object as function args to calculated properties sub __self__ { return $_[0]; } # Used to traverse n levels of indirect properties, even if the total # indirection is not defined on the primary ofhect this is called on. # For example: $obj->__get_attr__('a.b.c'); # gets $obj's 'a' value, calls 'b' on that, and calls 'c' on the last thing sub __get_attr__ { my ($self, $property_name) = @_; my @property_values; if (index($property_name,'.') == -1) { @property_values = $self->$property_name; } else { my @links = split(/\./,$property_name); @property_values = ($self); for my $full_link (@links) { my $pos = index($full_link,'-'); my $link = ($pos == -1 ? $full_link : substr($full_link,0,$pos) ); @property_values = map { defined($_) ? $_->$link : undef } @property_values; } } return if not defined wantarray; return @property_values if wantarray; if (@property_values > 1) { my $class_name = $self->__meta__->class_name; Carp::confess("Multiple values returned for $class_name $property_name in scalar context!"); } return $property_values[0]; } sub __label_name__ { # override to provide default labeling of the object my $self = $_[0]; my $class = ref($self) || $self; my ($label) = ($class =~ /([^:]+)$/); $label =~ s/([a-z])([A-Z])/$1 $2/g; $label =~ s/([A-Z])([A-Z]([a-z]|\s|$))/$1 $2/g; $label = uc($label) if $label =~ /_id$/i; return $label; } sub __display_name__ { # default stringification (does override "" unless you specifically choose to) my $self = shift; my $in_context_of_related_object = shift; my $name = $self->id; $name =~ s/\t/ /g; return $name; if (not $in_context_of_related_object) { # no in_context_of_related_object. # the object is identified globally return $self->label_name . ' ' . $name; } elsif ($in_context_of_related_object eq ref($self)) { # the class is completely known # show only the core display name # -> less text, more in_context_of_related_object return $name } else { # some intermediate base class is known, # TODO: make this smarter # For now, just show the whole class name with the ID return $self->label_name . ' ' . $name; } } sub __errors__ { # This is the basis for software constraint checking. # Return a list of values describing the problems on the object. my ($self,@property_names) = @_; my $class_object = $self->__meta__; unless (scalar @property_names) { @property_names = $class_object->all_property_names; } my @properties = map { $class_object->property_meta_for_name($_); } @property_names; my @tags; for my $property_metadata (@properties) { # For now we don't validate these. # Ultimately, we should delegate to the property metadata object for value validation. my($is_delegated, $is_calculated, $property_name, $is_optional, $generic_data_type, $data_length) = @$property_metadata{'is_delegated','is_calculated','property_name','is_optional', 'data_type','data_length'}; next if $is_delegated || $is_calculated; # TODO: is this making commits slow by calling lots of indirect accessors? my @values = $self->$property_name; next if @values > 1; my $value = $values[0]; if (! ($is_optional or defined($value))) { push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => "No value specified for required property", ); } # The tests below don't apply do undefined values. # Save the trouble and move on. next unless defined $value; # Check data type # TODO: delegate to the data type module for this $generic_data_type = '' unless (defined $generic_data_type); if ($generic_data_type eq 'Float') { $value =~ s/\s//g; $value = $value + 0; my $length =0; if($value =~ /^(\+|\-)?([0-9]+)(\.([0-9]*))?[eE](\+|\-)?(\d+)$/){ #-- scientific notation $length = length($2)-1 + $6 + (!$5 || $5 eq '+' ? 1 : 0); } elsif($value =~ /^(\+|\-)?([0-9]*)(\.([0-9]*))?$/) { # If the data type is specified as a Float, but really contains an int, then # $4 is undef causing a warning about "uninitialized value in concatenation", # but otherwise works OK no warnings 'uninitialized'; $length = length($2.$4); --$length if $2 == 0 && $4; } else{ push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => 'Invalid decimal value.' ); } # Cleanup for size check below. $value = '.' x $length; } elsif ($generic_data_type eq 'Integer') { $value =~ s/\s//g; if ($value =~ /\D/) { #$DB::single = 1; #print "$self $self->{id} $property_name @values\n"; } $value = $value + 0; if ($value !~ /^(\+|\-)?[0-9]*$/) { push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => 'Invalid integer.' ); } # Cleanup for size check below. $value =~ s/[\+\-]//g; } elsif ($generic_data_type eq 'DateTime') { # This check is currently disabled b/c of time format irrecularities # We rely on underlying database constraints for real invalidity checking. # TODO: fix me if (1) { } elsif ($value =~ /^\s*\d\d\d\d\-\d\d-\d\d\s*(\d\d:\d\d:\d\d|)\s*$/) { # TODO more validation here for a real date. } else { push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => 'Invalid date string.' ); } } # Check size if ($generic_data_type ne 'DateTime') { if ( defined($data_length) and ($data_length < length($value)) ) { push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => sprintf('Value too long (%s of %s has length of %d and should be <= %d).', $property_name, $self->$property_name, length($value), $data_length) ); } } # Check valid values if there is an explicit list if (my $constraints = $property_metadata->valid_values) { my $valid = 0; for my $valid_value (@$constraints) { no warnings; # undef == '' if ($value eq $valid_value) { $valid = 1; last; } } unless ($valid) { my $value_list = join(', ',@$constraints); push @tags, UR::Object::Tag->create( type => 'invalid', properties => [$property_name], desc => sprintf( 'The value %s is not in the list of valid values for %s. Valid values are: %s', $value, $property_name, $value_list ) ); } } # Check FK if it is easy to do. # TODO: This is a heavy weight check, and is disabled for performance reasons. # Ideally we'd check a foreign key value _if_ it was changed only, since # saved foreign keys presumably could not have been save if they were invalid. if (0) { my $r_class; unless ($r_class->get(id => $value)) { push @tags, UR::Object::Tag->create ( type => 'invalid', properties => [$property_name], desc => "$value does not reference a valid " . $r_class . '.' ); } } } return @tags; } # Standard API for working with UR fixtures # boolean expressions # sets # iterators # views # mock objects sub define_boolexpr { return UR::BoolExpr->resolve(@_); } sub define_set { my $class = shift; $class = ref($class) || $class; my $rule = UR::BoolExpr->resolve($class,@_); my $set_class = $class . "::Set"; return $set_class->get($rule->id); } sub add_observer { my $self = shift; my %params = @_; my $observer = UR::Observer->create( subject_class_name => $self->class, subject_id => (ref($self) ? $self->id : undef), %params, ); unless ($observer) { $self->error_message( "Failed to create observer: " . UR::Observer->error_message ); return; } return $observer; } sub remove_observers { my $self = shift; my %params = @_; my $aspect = delete $params{'aspect'}; my $callback = delete $params{'callback'}; if (%params) { Carp::croak('Unrecognized parameters for observer removal: ' . Data::Dumper::Dumper(\%params) . "Expected 'aspect' and 'callback'"); } my %args = ( subject_class_name => $self->class ); $args{'subject_id'} = $self->id if (ref $self); $args{'aspect'} = $aspect if (defined $aspect); $args{'callback'} = $callback if (defined $callback); my @observers = UR::Observer->get(%args); $_->delete foreach @observers; return @observers; } sub create_iterator { my $class = shift; # old syntax = create_iterator(where => [param_a => A, param_b => B]) if (@_ > 1) { my %params = @_; if (exists $params{'where'}) { Carp::carp('create_iterator called with old syntax create_iterator(where => \@params) should be called as create_iterator(@params)'); @_ = $params{'where'}; } } # new syntax, same as get() = create_iterator($bx) or create_iterator(param_a => A, param_b => B) my $filter; if (Scalar::Util::blessed($_[0]) && $_[0]->isa('UR::BoolExpr')) { $filter = $_[0]; } else { $filter = UR::BoolExpr->resolve($class, @_) } my $iterator = UR::Object::Iterator->create_for_filter_rule($filter); unless ($iterator) { $class->error_message(UR::Object::Iterator->error_message); return; } return $iterator; } sub create_view { my $self = shift; my $class = $self->class; # this will auto-subclass into ${class}::View::${perspective}::${toolkit}, # using $class or some parent class of $class my $view = UR::Object::View->create( subject_class_name => $class, perspective => "default", @_ ); unless ($view) { $self->error_message("Error creating view: " . UR::Object::View->error_message); return; } if (ref($self)) { $view->subject($self); } return $view; } sub create_mock { my $class = shift; my %params = @_; require Test::MockObject; my $self = Test::MockObject->new(); my $subject_class_object = $class->__meta__; for my $class_object ($subject_class_object,$subject_class_object->ancestry_class_metas) { for my $property ($class_object->direct_property_metas) { my $property_name = $property->property_name; if ($property->is_delegated && !exists($params{$property_name})) { next; } if ($property->is_mutable || $property->is_calculated || $property->is_delegated) { my $sub = sub { my $self = shift; if (@_) { if ($property->is_many) { $self->{'_'. $property_name} = @_; } else { $self->{'_'. $property_name} = shift; } } return $self->{'_'. $property_name}; }; $self->mock($property_name, $sub); if ($property->is_optional) { if (exists($params{$property_name})) { $self->$property_name($params{$property_name}); } } else { unless (exists($params{$property_name})) { if (defined($property->default_value)) { $params{$property_name} = $property->default_value; } else { unless ($property->is_calculated) { Carp::croak("Failed to provide value for required mutable property '$property_name'"); } } } $self->$property_name($params{$property_name}); } } else { unless (exists($params{$property_name})) { if (defined($property->default_value)) { $params{$property_name} = $property->default_value; } else { Carp::croak("Failed to provide value for required property '$property_name'"); } } if ($property->is_many) { $self->set_list($property_name,$params{$property_name}); } else { $self->set_always($property_name,$params{$property_name}); } } } } my @classes = ($class, $subject_class_object->ancestry_class_names); $self->set_isa(@classes); $UR::Context::all_objects_loaded->{$class}->{$self->id} = $self; return $self; } # Typically only used internally by UR except when debugging. sub __changes__ { # Return a list of changes present on the object _directly_. # This is really only useful internally because the boundary of the object # is internal/subjective. my ($self,$optional_property) = @_; # performance optimization return unless $self->{_change_count}; unless (wantarray) { return $self->{_change_count}; # scalar context only cares if there are any changes or not } my $meta = $self->__meta__; if (ref($meta) eq 'UR::DeletedRef') { print Data::Dumper::Dumper($self,$meta); Carp::confess("Meta is deleted for object requesting changes: $self\n"); } if (!$meta->is_transactional and !$meta->is_meta_meta) { return; } my $orig = $self->{db_saved_uncommitted} || $self->{db_committed}; no warnings; my @changed; if ($orig) { my $class_name = $meta->class_name; @changed = grep { my $property_meta = $meta->property_meta_for_name($_); ( ((!$property_meta) or $property_meta->is_transient) ? 0 : 1 ); } grep { $self->can($_) and not UR::Object->can($_) } grep { $orig->{$_} ne $self->{$_} } grep { $_ } keys %$orig; } else { @changed = $meta->all_property_names } return map { UR::Object::Tag->create ( type => 'changed', properties => [$_] ) } @changed; } sub _changed_property_names { my $self = shift; my @changes = $self->__changes__; my %changed_properties; foreach my $change ( @changes ) { next unless ($change->type eq 'changed'); $changed_properties{$_} = 1 foreach $change->properties; } return keys %changed_properties; } sub __signal_change__ { # all mutable property accessors ("setters") call this method to tell the # current context about a state change. $UR::Context::current->add_change_to_transaction_log(@_); $UR::Context::current->send_notification_to_observers(@_); } # send notifications that aren't state changes to observers sub __signal_observers__ { $UR::Context::current->send_notification_to_observers(@_); } sub __define__ { # This is used internally to "virtually load" things. # Simply assert they already existed externally, and act as though they were just loaded... # It is used for classes defined in the source code (which is the default) by the "class {}" magic # instead of in some database, as we'd do for regular objects. It is also used by some test cases. if ($UR::initialized and $_[0] ne 'UR::Object::Property') { # the nornal implementation has all create() features my $self; do { local $UR::Context::construction_method = '__define__'; $self = $UR::Context::current->create_entity(@_); }; return unless $self; $self->{db_committed} = { %$self }; $self->{'__defined'} = 1; $self->__signal_change__("load"); return $self; } else { # used during boostrapping my $class = shift; my $class_meta = $class->__meta__; if (my $method_name = $class_meta->sub_classification_method_name) { my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, @_); my $sub_class_name = $class->$method_name(@_); if ($sub_class_name ne $class) { # delegate to the sub-class to create the object return $sub_class_name->__define__(@_); } } my $self = $UR::Context::current->_construct_object($class, @_); return unless $self; $self->{db_committed} = { %$self }; $self->__signal_change__("load"); return $self; } } sub __extend_namespace__ { # A class Foo can implement this method to have a chance to auto-define Foo::Bar # TODO: make a Class::Autouse::ExtendNamespace Foo => sub { } to handle this. # Right now, UR::ModuleLoader will try it after "use". my $class = shift; my $ext = shift; my $class_meta = $class->__meta__; return $class_meta->generate_support_class_for_extension($ext); } # Handling of references within the current process sub __weaken__ { # Mark this object as unloadable by the object cache pruner. # If the class has a data source, then a weakened object is dropped # at the first opportunity, reguardless of its __get_serial number. # For classes without a data source, then it will be dropped according to # the normal rules w/r/t the __get_serial (classes without data sources # normally are never dropped by the pruner) my $self = $_[0]; delete $self->{'__strengthened'}; $self->{'__weakened'} = 1; } sub __strengthen__ { # Indicate this object should never be unloaded by the object cache pruner my $self = $_[0]; delete $self->{'__weakened'}; $self->{'__strengthened'} = 1; } sub DESTROY { # Handle weak references in the object cache. my $obj = shift; # $destroy_should_clean_up_all_objects_loaded will be true if either light_cache is on, or # the cache_size_highwater mark is a valid value if ($UR::Context::destroy_should_clean_up_all_objects_loaded) { my $class = ref($obj); my $obj_from_cache = delete $UR::Context::all_objects_loaded->{$class}{$obj->{id}}; if ($obj->__meta__->is_meta_meta or @{[$obj->__changes__]}) { die "Object found in all_objects_loaded does not match destroyed ref/id! $obj/$obj->{id}!" unless $obj eq $obj_from_cache; $UR::Context::all_objects_loaded->{$class}{$obj->{id}} = $obj; print "KEEPING $obj. Found $obj .\n"; return; } else { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY object $obj class ",$obj->class," id ",$obj->id,"\n"; } $obj->unload(); return $obj->SUPER::DESTROY(); } } else { if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM DESTROY object $obj class ",$obj->class," id ",$obj->id,"\n"; } $obj->SUPER::DESTROY(); } }; END { # Turn off monitoring of the DESTROY handler at application exit. # setting the typeglob to undef does not work. -sms delete $UR::Object::{DESTROY}; }; # This module implements the deprecated parts of the UR::Object API require UR::ObjectDeprecated; 1; =pod =head1 NAME UR::Object - transactional, queryable, process-independent entities =head1 SYNOPSIS Create a new object in the current context, and return it: $elmo = Acme::Puppet->create( name => 'Elmo', father => $ernie, mother => $bigbird, jobs => [$dance, $sing], favorite_color => 'red', ); Plain accessors work in the typial fashion: $color = $elmo->favorite_color(); Changes occur in a transaction in the current context: $elmo->favorite_color('blue'); Non-scalar (has_many) properties have a variety of accessors: @jobs = $elmo->jobs(); $jobs = $elmo->job_arrayref(); $set = $elmo->job_set(); $iter = $elmo->job_iterator(); $job = $elmo->add_job($snore); $success = $elmo->remove_job($sing); Query the current context to find objects: $existing_obj = Acme::Puppet->get(name => 'Elmo'); # same reference as $existing_obj @existing_objs = Acme::Puppet->get( favorite_color => ['red','yellow'], ); # this will not get elmo because his favorite color is now blue @existing_objs = Acme::Puppet->get(job => $snore); # this will return $elmo along with other puppets that snore, # though we haven't saved the change yet.. Save our changes: UR::Context->current->commit; Too many puppets...: $elmo->delete; $elmo->play; # this will throw an exception now $elmo = Acme::Puppet->get(name => 'Elmo'); # this returns nothing now Just kidding: UR::Context->current->rollback; # not a database rollback, an in-memory undo All is well: $elmo = Acme::Puppet->get(name => 'Elmo'); # back again! =head1 DESCRIPTION UR::Objects are transactional, queryable, representations of entities, built to maintain separation between the physical reference in a program, and the logical entity the reference represents, using a well-defined interface. UR uses that separation to automatically handle I/O. It provides a query API, and manages the difference between the state of entities in the application, and their state in external persistance systems. It aims to do so transparently, keeping I/O logic orthogonally to "business logic", and hopefully making code around I/O unnecessary to write at all for most programs. Rather than explicitly constructing and serializing/deserializing objects, the application layer just requests objects from the current "context", according to their characteristics. The context manages database connections, object state changes, references, relationships, in-memory transactions, queries and caching in tunable ways. Accessors dynamically fabricate references lazily, as needed through the same query API, so objects work as the developer would traditionally expect in most cases. The goal of UR::Object is that your application doesn't have to do data management. Just ask for what you want, use it, and let it go. UR::Objects support full reflection and meta-programming. Its meta-object layer is fully self-bootstrapping (most classes of which UR is composed are themselves UR::Objects), so the class data can introspect itself, such that even classes can be created within transactions and discarded. =head1 INHERITANCE UR::ModuleBase Basic error, warning, and status messages for modules in UR. UR::Object This class - general OO transactional OO features =head1 WRITING CLASSES See L for a narrative explanation of how to write clases. For a complete reference see L. For the meta-object API see L. A simple example, declaring the class used above: class Acme::Puppet { id_by => 'name', has_optional => [ father => { is => 'Acme::Puppet' }, mother => { is => 'Acme::Puppet' }, jobs => { is => 'Acme::Job', is_many => 1 }, ] }; You can also declare the same API, but specifying additional internal details to make database mapping occur the way you'd like: class Acme::Puppet { id_by => 'name', has_optional => [ father => { is => 'Acme::Puppet', id_by => 'father_id' }, mother => { is => 'Acme::Puppet', id_by => 'mother_id' }, }, has_many_optional => [ job_assignments => { is => 'Acme::PuppetJob', im_its => 'puppet' }, jobs => { is => 'Acme::Job', via => 'job_assignments', to => 'job' }, ] }; =head1 CONSTRUCTING OBJECTS New objects are returned by create() and get(), which delegate to the current context for all object construction. The create() method will always create something new or will return undef if the identity is already known to be in use. The get() method lets the context internally decide whether to return a cached reference for the specified logical entities or to construct new objects by loading data from the outside. =head1 METHODS The examples below use $obj where an actual object reference is required, and SomeClass where the class name can be used. In some cases the example in the synopsisis is continued for deeper illustration. =head2 Base API =over 4 =item get $obj = SomeClass->get($id); $obj = SomeClass->get(property1 => value1, ...); @obj = SomeClass->get(property1 => value1, ...); @obj = SomeClass->get('property1 operator1' => value1, ...); Query the current context for objects. It turns the passed-in parameters into a L and returns all objects of the given class which match. The current context determines whether the request can be fulfilled without external queries. Data is loaded from underlying database(s) lazliy as needed to fulfuill the request. In the simplest case of requesting an object by id which is cached, the call to get() is an immediate hash lookup, and is very fast. See L, or look at L, L, and L for details. If called in scalar context and more than one object matches the given parameters, get() will raise an exception through C. =item create $obj = SomeClass->create( property1 => $value1, properties2 => \@values2, ); Create a new entity in the current context, and return a reference to it. The only required property to create an object is the "id", and that is only required for objects which do not autogenerate their own ids. This requirement may be overridden in subclasses to be more restrictive. If entities of this type persist in an underlying context, the entity will not appear there until commit. (i.e. no insert is done until just before a real database commit) The object in question does not need to pass its own constraints when initially created, but must be fully valid before the transaction which created it commits. =item delete $obj->delete Deletes an object in the current context. The $obj reference will be garbage collected at the discretion of the Perl interpreter as soon as possible. Any attempt to use the reference after delete() is called will result in an exception. If the represented entity was loaded from the parent context (i.e. persistent database objects), it will not be deleted from that context (the database) until commit is called. The commit call will do both the delete and the commit, presuming the complete save works across all involved data sources. Should the transaction roll-back, the deleted object will be re-created in the current context, and a fresh reference will later be returnable by get(). See the documentation on L for details on how deleted objects are rememberd and removed later from the database, and how deleted objects are re-constructed on STM rollback. =item class $class_name = $obj->class; $class_name = SomeClass->class; Returns the name of the class of the object in question. See __meta__ below for the class meta-object. =item id $id = $obj->id; The unique identifier of the object within its class. For database-tracked entities this is the primary key value, or a composite blob containing the primary key values for multi-column primary keys. For regular objects private to the process, the default id embeds the hostname, process ID, and a timestamp to uniquely identify the UR::Context::Process object which is its final home. When inheritance is involved beneath UR::Object, the 'id' may identify the object within the super-class as well. It is also possible for an object to have a different id upon sub-classification. =back =head2 Accessors Every relationship declared in the class definition results in at least one accesor being generated for the class in question. Identity properties are read-only, while non-identity properties are read-write unless is_mutable is explicitly set to false. Assigning an invalid value is allowed temporarily, but the current transaction will be in an invalid state until corrected, and will not be commitable. The return value of an the accessor when it mutates the object is the value of the property after the mutation has occurred. =head3 Single-value property accessors: By default, properties are expected to return a single value. =over 4 =item NAME Regular accessors have the same name as the property, as declared, and also work as mutators as is commonly expected: $value = $obj->property_name; $obj->property_name($new_value); When the property is declared with id_by instead of recording the refereince, it records the id of the object automatically, such that both will return different values after either changes. =back =head3 Muli-value property accessors: When a property is declared with the "is_many" flag, a variety of accessors are made available on the object. See C for more details on the ways to declare relationships between objects when writing classes. Using the example from the synopsis: =over 4 =item NAMEs (the property name pluralized) A "has_many" relationship is declared using the plural form of the relationship name. An accessor returning the list of property values is generated for the class. It is usable with or without additional filters: @jobs = $elmo->jobs(); @fun_jobs = $elmo->jobs(is_fun => 1); The singular name is used for the remainder of the accessors... =item NAME (the property name in singular form) Returns one item from the group, which must be specified in parameters. If more than one item is matched, an exception is thrown via die(): $job = $elmo->job(name => 'Sing'); $job = $elmo->job(is_fun => 1); # die: too many things are fun for Elmo =item NAME_list The default accessor is available as *_list. Usable with or without additional filters: @jobs = $elmo->job_list(); @fun_jobs = $elmo_>job_list(is_fun => 1); =item NAME_set Return a L value representing the values with *_set: $set = $elmo->job_set(); $set = $elmo->job_set(is_hard => 1); =item NAME_iterator Create a new iterator for the set of property values with *_iterator: $iter = $elmo->job_iterator(); $iter = $elmo->job_iterator(is_fun => 1, -order_by => ['name]); while($obj = $iter->next()) { ... } =item add_NAME Add an item to the set of values with add_*: $added = $elmo->add_job($snore); A variation of the above will construt the item and add it at once. This second form of add_* automatically would identify that the line items also reference the order, and establish the correct converse relationship automatically. @lines = $order->lines; # 2 lines, for instance $line = $order->add_line( product => $p, quantity => $q, ); print $line->num; # 3, if the line item has a multi-column primary key with auto_increment on the 2nd column called num =item remove_NAME Items can be removed from the assigned group in a way symetrical with how they are added: $removed = $elmo->remove_job($sing); =back =head2 Extended API These methods are available on any class defined by UR. They are convenience methods around L, L, L, L, L and L. =over 4 =item create_iterator $iter = SomeClass->create_iterator( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); while (my $obj = $iter->next) { ... } Takes the same sort of parameters as get(), but returns a L for the matching objects. The next() method will return one object from the resulting set each time it is called, and undef when the results have been exhausted. C instances are normal object references in the current process, not context-oriented UR::Objects. They vanish upon dereference, and cannot be retrieved by querying the context. When using an iterator, the system attempts to return objects matching the params at the time the iterator is created, even if those objects do not match the params at the time they are returned from next(). Consider this case: # many objects in the DB match this my $iter = SomeClass->create_iterator(job => 'cleaner'); my $an_obj = SomeClass->get(job => 'cleaner', id => 1); $an_obj->job('messer-upper'); # This no longer matches the iterator's params my @iter_objs; while (my $o = $iter->next) { push @iter_objs, $o; } At the end, @iter_objs will contain several objects, including the object with id 1, even though its job is no longer 'cleaner'. However, if an object matching the iterator's params is deleted between the time the iterator is created and the time next() would return that object, then next() will throw an exception. =item define_set $set = SomeClass->define_set( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); @subsets = $set->group_by('property3','property4'); @some_members = $subsets[0]->members; Takes the same sort of parameters as get(), but returns a set object. Sets are lazy, and only query underlying databases as much as necessary. At any point in time the members() method returns all matches to the specified parameters. See L for details. =item define_boolexpr $bx = SomeClass->define_boolexpr( property1 => $explicit_value, property2 => \@my_in_clause, 'property3 like' => 'some_pattern_with_%_as_wildcard', 'property4 between' => [$low,$high], ); $bx->evaluate($obj1); # true or false? Takes the same sort of parameters as get(), but returns a L object. The boolean expression can be used to evaluate other objects to see if they match the given condition. The "id" of the object embeds the complete "where clause", and as a semi-human-readable blob, such is reconstitutable from it. See L for details on how to use this to do advanced work on defining sets, comparing objects, creating query templates, adding object constraints, etc. =item add_observer $o = $obj1->add_observer( aspect => 'someproperty' callback => sub { print "change!\n" }, ); $obj1->property1('new value'); # observer callback fires.... $o->delete; Adds an observer to an object, monitoring one or more of its properties for changes. The specified callback is fired upon property changes which match the observation request. =item create_mock $mock = SomeClass->create_mock( property1 => $value, method1 => $return_value, ); Creates a mock object using using the class meta-data for "SomeClass" via L. Useful for test cases. =back =head2 Meta API The folowing methods allow the application to interrogate UR for information about the object in question. =over 4 =item __meta__ $class_obj = $obj->__meta__(); Returns the class metadata object for the given object's class. Class objects are from the class L, and hold information about the class' properties, data source, relationships to other classes, etc. =item __extend_namespace__ package Foo::Bar; class Foo::Bar { has => ['stuff','things'] }; sub __extend_namespace__ { my $class = shift; my $ext = shift; return class {$class . '::' . $ext} { has => ['more'] }; } Dynamically generate new classes under a given namespace. This is called automatically by UR::ModuleLoader when an unidentified class name is used. If Foo::Bar::Baz is not a UR class, and this occurs: Foo::Bar::Baz->some_method() This is called: Foo::Bar->__extend_namespace__("Baz") If it returns a new class meta, the code will proceed on as though the class had always existed. If Foo::Bar does not exist, the above will be called recursively: Foo->__extend_namespace__("Bar") If Foo::Bar, whether loaded or generated, cannot extend itself for "Baz", the loader will go up the tree before giving up. This means a top-level module could dynamically define classes for any given class name used under it: Foo->__extend_namespace__("Bar::Baz") =item __errors__ @tags = $obj->__errors__() Return a list of L values describing the issues which would prevent a commit in the current transaction. The base implementation check the validity of an object by applying any constraints layed out in the class such as making sure any non-optional properties contain values, numeric properties contain numeric data, and properties with enumerated values only contain valid values. Sub-classes can override this method to add additional validity checking. =item __display_name__ $text = $obj->__display_name__; # the class and id of $obj, by default $text = $line_item->__display_name__($order); Stringifies an object. Some classes may choose to actually overload the stringification operator with this method. Even if they do not, this method will still attempt to identify this object in text form. The default returns the class name and id value of the object within a string. It can be overridden to do a more nuanced job. The class might also choose to overload the stringification operator itself with this method, but even if it doesn not the system will presume this method can be called directly on an object for reasonable stringificaiton. =item __context__ $c = $self->__context__; Return the L for the object reference in question. In UR, a "context" handles connextions between objects, instead of relying on having objects directly reference each other. This allows an object to have a relationship with a large number of other logical entities, without having a "physical" reference present within the process in question. All attempts to resolve non-primitive attribute access go through the context. =back =head2 Extension API These methods are primarily of interest for debugging, for test cases, and internal UR development. They are likely to change before the 1.0 release. =over 4 =item __signal_change__ Called by all mutators to tell the current context about a state change. =item __changes__ @tags = $obj->__changes__() Return a list of changes present on the object _directly_. This is really only useful internally because the boundary of the object is internal/subjective. Changes to objects' properties are tracked by the system. If an object has been changed since it was defined or loaded from its external data source, then changed() will return a list of L objects describing which properties have been changed. Work is in-progress on an API to request the portion of the changes in effect in the current transaction which would impact the return value of a given list of properties. This would be directly usable by a view/observer. =item __define__ This is used internally to "virtually load" things. Simply assert they already existed externally, and act as though they were just loaded... It is used for classes defined in the source code (which is the default) by the "class {}" magic instead of in some database, as we'd do for regular objects. =item __strengthen__ $obj->__strengthen__(); Mark this object as unloadable by the object cache pruner. UR objects are normally tracked by the current Context for the life of the application, but the programmer can specify a limit to cache size, in which case old, unchanged objects are periodically pruned from the cache. If strengthen() is called on an object, it will effectively be locked in the cache, and will not be considered for pruning. See L for more information about the pruning mechanism. =item __weaken__ $obj->__weaken__(); Give a hint to the object cache pruner that this instance is not going to be used in the application in the future, and should be removed with preference when pruning the cache. =item DESTROY Perl calls this method on any object before garbage collecting it. It should never by called by your application explicitly. The DESTROY handler is overridden in UR::Object. If you override it in a subclass, be sure to call $self->SUPER::DESTROY() before exiting your override, or errors will occur. =back =head1 ERRORS, WARNINGS and STATUS MESSAGES When an error occurs which is "exceptional" the API will throw an exception via die(). In some cases, when the possibility of failure is "not-exceptional", the method will simply return false. In scalar context this will be undef. In list context an empty list. When there is ambiguity as to whether this is an error or not (get() for instance, might simply match zero items, ...or fail to understand your parameters), an exception is used. =over 4 =item error_message The standard way to convey the error which has occurred is to set ->error_message() on the object. This will propagate to the class, and through its inheritance. This is much like DBI's errstr method, which affects the handle on which it was called, its source handle, and the DBI package itself. =item warning_message Calls to warning_message also record themselves on the object in question, and its class(es). They also emit a standard Perl warn(), which will invoke $SIG{__WARN__}; =item status_message Calls to status_message are also recorded on the object in question. They can be monitored through hooks, as can the other messages. =back See L for more information. =head1 SEE ALSO L, L, L L, L, L, L L contains additional methods which are deprecated in the API. =cut Namespace.pm000444023532023421 1705212121654174 15174 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Namespace; use strict; use warnings; use File::Find; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Namespace', is => ['UR::Singleton'], is_abstract => 1, has => [ domain => { is => 'Text', is_optional => 1, len => undef, doc => "DNS domain name associated with the namespace in question", }, allow_sloppy_primitives => { is => 'Boolean', default_value => 1, doc => 'when true, unrecognized data types will function as UR::Value::SloppyPrimitive' }, method_resolution_order => { is => 'Text', value => ($^V lt v5.9.5 ? 'dfs' : 'c3'), valid_values => ($^V lt v5.9.5 ? ['dfs'] : ['dfs', 'c3']), doc => 'Method Resolution Order to use for this namespace. C3 is only supported in Perl >= 5.9.5.', }, ], doc => 'The class for a singleton module representing a UR namespace.', ); sub get_member_class { my $self = shift; return UR::Object::Type->get(@_); } # FIXME These should change to using the namespace metadata DB when # that's in place, rather than trolling through the directory tree sub get_material_classes { my $self = shift->_singleton_object; my @classes; if (my $cached = $self->{material_classes}) { @classes = map { UR::Object::Type->get($_) } @$cached; } else { my @names; for my $class_name ($self->_get_class_names_under_namespace()) { my $class = eval { UR::Object::Type->get($class_name) }; next unless $class; push @classes, $class; push @names, $class_name; } $self->{material_classes} = \@names; } return @classes; } # Subclasses can override this method to tell the dynamic module loader # whether it should go ahead and load the given module name or not. # The default behavior is to go ahead and try for them all sub should_dynamically_load_class { # my($self,$class_name) = @_; return 1; } sub get_material_class_names { return map {$_->class_name} $_[0]->get_material_classes(); } # Returns data source objects for all the data sources of the namespace sub get_data_sources { my $class = shift; if ($class eq 'UR' or (ref($class) and $class->id eq 'UR')) { return 'UR::DataSource::Meta'; # UR only has 1 "real" data source, the other stuff in that dir are base classes } else { my %found; my $namespace_name = $class->class; foreach my $inc ( @main::INC ) { my $path = join('/', $inc,$namespace_name,'DataSource'); if (-d $path) { foreach ( glob($path . '/*.pm') ) { my($module_name) = m/DataSource\/([^\/]+)\.pm$/; my $ds_class_name = $namespace_name . '::DataSource::' . $module_name; $found{$ds_class_name} = 1; } } } my @data_sources = map { $_->get() } keys(%found); return @data_sources; } } sub get_base_contexts { return shift->_get_class_names_under_namespace("Context"); } sub get_vocabulary { my $class = shift->_singleton_class_name; return $class . "::Vocabulary"; } sub get_base_directory_name { my $class = shift->_singleton_class_name; my $dir = $class->__meta__->module_path; $dir =~ s/\.pm$//; return $dir; } sub get_deleted_module_directory_name { my $self = shift; my $meta = $self->__meta__; my $path = $meta->module_path; $path =~ s/.pm$//g; $path .= "/.deleted"; return $path; } # FIXME This is misnamed... # It really returns all the package names under the specified directory # (assumming the packages defined in the found files are named like the # pathname of the file), not just those that implement classes sub _get_class_names_under_namespace { my $class = shift->_singleton_class_name; my $subdir = shift; Carp::confess if ref($class); my $dir = $class->get_base_directory_name; my $namespace_dir; if (defined($subdir) and length($subdir)) { $namespace_dir = join("/",$dir, $subdir); } else { $namespace_dir = $dir; } my $namespace = $class; my %class_names; my $preprocess = sub { if ($File::Find::dir =~ m/\/t$/) { return (); } elsif (-e ($File::Find::dir . "/UR_IGNORE")) { return (); } else { return @_; } }; my $wanted = sub { return if -d $File::Find::name; # not interested in directories return if $File::Find::name =~ /\/\.deleted\//; # .deleted directories are created by ur update classes return if -e $File::Find::name . '/UR_IGNORE'; # ignore a whole directory? return unless $File::Find::name =~ m/\.pm$/; # must be a perl module return unless $File::Find::name =~ m/.*($namespace\/.*)\.pm/; my $try_class = $1; return if $try_class =~ m([^\w/]); # Skip names that make for illegal package names. Must be word chars or a / $try_class =~ s/\//::/g; $class_names{$try_class} = 1 if $try_class; }; my @dirs_to_search = @INC; my $path_to_check = $namespace; $path_to_check .= "/$subdir" if $subdir; @dirs_to_search = map($_ . '/' . $path_to_check, @dirs_to_search); # only look in places with namespace_name as a subdir unshift(@dirs_to_search, $namespace_dir) if (-d $namespace_dir); @dirs_to_search = grep { $_ =~ m/\/$path_to_check/ and -d $_ } @dirs_to_search; return unless @dirs_to_search; find({ wanted => $wanted, preprocess => $preprocess }, @dirs_to_search); return sort keys %class_names; } 1; =pod =head1 NAME UR::Namespace - Manage collections of packages and classes =head1 SYNOPSIS In a file called MyApp.pm: use UR; UR::Object::Type->define( class_name => 'MyApp', is => 'UR::Namespace', ); Other programs, as well as modules in the MyApp subdirectory can now put use MyApp; in their code, and they will have access to all the classes and data under the MyApp tree. =head1 DESCRIPTION A UR namespace is the top-level object that represents your data's class structure in the most general way. After use-ing a namespace module, the program gets access to the module autoloader, which will automatically use modules on your behalf if you attempt to interact with their packages in a UR-y way, such as calling get(). Most programs will not interact with the Namespace, except to C its package. =head1 Methods =over 4 =item get_material_classes my @class_metas = $namespace->get_material_classes(); Return a list of L class metadata object that exist in the given Namespace. Note that this uses File::Find to find C<*.pm> files under the Namespace directory and calls Cget($name)> for each package name to get the autoloader to use the package. It's likely to be pretty slow. =item get_material_class_names my @class_names = $namespace->get_material_class_names() Return just the names of the classes produced by C. =item get_data_sources my @data_sources = $namespace->get_data_sources() Return the data source objects it finds defined under the DataSource subdirectory of the namespace. =item get_base_directory_name my $path = $namespace->get_base_directory_name() Returns the directory path where the Namespace module was loaded from. =back =head1 SEE ALSO L, L, L =cut Debug.pm000444023532023421 52212121654175 14261 0ustar00abrummetgsc000000000000UR-0.41/lib/URpackage UR::Debug; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; our $verify_indexes = 0; 1; =pod =head1 NAME UR::Debug - Controls for debugging behavior =head1 DESCRIPTION Flags which turn on debugging behavior are set here. Change them in the debugger to activate debugging functionality. =cut ModuleLoader.pm000444023532023421 736412121654175 15642 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::ModuleLoader; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; Class::Autouse->autouse(\&dynamically_load_class); Class::Autouse->sugar(\&define_class); my %loading; sub define_class { my ($class,$func,@params) = @_; return unless $UR::initialized; return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get"); #return if $loading{$class}; #$loading{$class} = 1; # Handle the special case of defining a new class # This lets us have the effect of a UNIVERSAL::class method, w/o mucking with UNIVERSAL if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") { my @class_params; if (@params == 2 and ref($params[1]) eq 'HASH') { @class_params = %{ $params[1] }; } elsif (@params == 2 and ref($params[1]) eq 'ARRAY') { @class_params = @{ $params[1] }; } else { @class_params = @params[1..$#params]; } my $class_meta = UR::Object::Type->define(class_name => $class, @class_params); unless ($class_meta) { die "error defining class $class!"; } return sub { $class }; } else { return; } } sub dynamically_load_class { my ($class,$func,@params) = @_; # Don't even try to load unless we're done boostrapping somewhat. return unless $UR::initialized; return unless $Class::Autouse::ORIGINAL_CAN->("UR::Object::Type","get"); # Some modules (Class::DBI, recently) call UNIVERSAL::can directly with things which don't even resemble # class names. Skip doing any work on anything which isn't at least a two-part class name. # We refuse explicitly to handle top-level namespaces below anyway, and this will keep us from # slowing down other modules just to fail late. my ($namespace) = ($class =~ /^(.*?)::/); return unless $namespace; if (defined($func) and $func eq "class" and @params > 1 and $class ne "UR::Object::Type") { # a "class" statement caught by the above define_class call return; } unless ($namespace->isa("UR::Namespace")) { return; } # TODO: this isn't safe against exceptions # Instead, localize %loading with a copy of the previous %loading plus one class return if $loading{$class}; $loading{$class} = 1; unless ($namespace->should_dynamically_load_class($class)) { delete $loading{$class}; return; } # Attempt to get a class object, loading it as necessary (probably). # TODO: this is a non-standard accessor my $meta = $namespace->get_member_class($class); unless ($meta) { delete $loading{$class}; return; } # Handle the case in which the class is not "generated". # These are generated by default when used, so this is a corner case. unless ($meta->generated()) { # we have a new class # attempt to auto-generate it unless ($meta->generate) { Carp::confess("failed to auto-generate $class"); } } delete $loading{$class}; # Return a descriptive error message for the caller. my $fref; if (defined $func) { $fref = $class->can($func); unless ($fref) { Carp::confess("$class was auto-generated successfully but cannot find method $func"); } return $fref; } return 1; }; 1; =pod =head1 NAME UR::ModuleLoader - UR hooks into Class::Autouse =head1 DESCRIPTION UR uses Class::Autouse to handle automagic loading for modules. As long as some part of an application "use"s a Namespace module, the autoloader will handle loading modules under that namespace when they are needed. =head1 SEE ALSO UR, UR::Namespace =cut Observer.pm000444023532023421 2715212121654175 15072 0ustar00abrummetgsc000000000000UR-0.41/lib/UR package UR::Observer; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, has => [ subject_class => { is => 'UR::Object::Type', id_by => 'subject_class_name' }, subject_id => { is => 'SCALAR', is_optional => 1 }, subject => { is => 'UR::Object', calculate_from => ['subject_class_name','subject_id'], calculate => '$subject_class_name->get($subject_id)' }, aspect => { is => 'String', is_optional => 1 }, priority => { is => 'Number', is_optional => 1, default_value => 1 }, note => { is => 'String', is_optional => 1 }, ], is_transactional => 1, ); # This is not implemented as a "real" observer via create() because at the point during bootstrapping # that this module is loaded, we're not yet ready to start creating objects __PACKAGE__->_insert_record_into_all_change_subscriptions('UR::Observer', 'priority', '', [\&_modify_priority, '', 0, UR::Object::Type->autogenerate_new_object_id_uuid]); sub create { my $class = shift; $class->_create_or_define('create', @_); } sub __define__ { my $class = shift; $class->_create_or_define('__define__', @_); } sub _create_or_define { my $class = shift; my $method = shift; my ($rule,%extra) = UR::BoolExpr->resolve($class,@_); my $callback = delete $extra{callback}; unless ($callback) { $class->error_message("'callback' is a required parameter for creating UR::Observer objects"); return; } if (%extra) { $class->error_message("Cannot create observer. Class $class has no property ".join(',',keys %extra)); return; } my $subject_class_name = $rule->value_for('subject_class_name'); my $subject_class_meta = eval { $subject_class_name->__meta__ }; if ($@) { $class->error_message("Can't create observer with subject_class_name '$subject_class_name': Can't get class metadata for class '$subject_class_name': $@"); return; } unless ($subject_class_meta) { $class->error_message("Class $subject_class_name cannot be the subject class for an observer because there is no class metadata"); return; } my $aspect = $rule->value_for('aspect'); my $subject_id = $rule->value_for('subject_id'); unless ($subject_class_meta->_is_valid_signal($aspect)) { if ($subject_class_name->can('validate_subscription') and ! $subject_class_name->validate_subscription($aspect, $subject_id, $callback)) { $class->error_message("'$aspect' is not a valid aspect for class $subject_class_name"); return; } } if (!defined($subject_class_name) or $subject_class_name eq 'UR::Object') { $subject_class_name = '' }; # This was part of the old API, not sure why it's still here?! if (!defined ($aspect)) { $aspect = '' }; if (!defined ($subject_id)) { $subject_id = '' }; my $self; if ($method eq 'create') { $self = $class->SUPER::create($rule); } elsif ($method eq '__define__') { $self = $class->SUPER::__define__($rule->params_list); } else { Carp::croak('Instantiating a UR::Observer with some method other than create() or __define__() is not supported'); } $self->{callback} = $callback; my %params = $rule->params_list; my ($subscription, $delete_subscription); $self->_insert_record_into_all_change_subscriptions($subject_class_name, $aspect, $subject_id, [$callback, $self->note, $self->priority, $self->id]); return $self; } sub _insert_record_into_all_change_subscriptions { my($class,$subject_class_name, $aspect,$subject_id, $new_record) = @_; my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id} ||= []; push @$list, $new_record; } sub _modify_priority { my($self, $aspect, $old_val, $new_val) = @_; my $subject_class_name = $self->subject_class_name; my $subject_aspect = $self->aspect; my $subject_id = $self->subject_id; my $list = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$subject_aspect}->{$subject_id}; return unless $list; # this is probably an error condition my $data; for (my $i = 0; $i < @$list; $i++) { if ($list->[$i]->[3] eq $self->id) { ($data) = splice(@$list,$i, 1); last; } } return unless $data; # This is probably an error condition... $data->[2] = $new_val; $self->_insert_record_into_all_change_subscriptions($subject_class_name, $subject_aspect, $subject_id, $data); } sub callback { shift->{callback}; } sub subscription { shift->{subscription} } sub delete { my $self = shift; #$DB::single = 1; my $subject_class_name = $self->subject_class_name; my $subject_id = $self->subject_id; my $aspect = $self->aspect; $subject_class_name = '' if (! $subject_class_name or $subject_class_name eq 'UR::Object'); $subject_id = '' unless (defined $subject_id); $aspect = '' unless (defined $aspect); my $arrayref = $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id}; if ($arrayref) { my $index = 0; while ($index < @$arrayref) { if ($arrayref->[$index]->[3] eq $self->id) { my $found = splice(@$arrayref,$index,1); if (@$arrayref == 0) { $arrayref = undef; delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}->{$subject_id}; if (keys(%{ $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect} }) == 0) { delete $UR::Context::all_change_subscriptions->{$subject_class_name}->{$aspect}; } } # old API unless ($subject_class_name eq '' || $subject_class_name->inform_subscription_cancellation($aspect,$subject_id,$self->{'callback'})) { Carp::confess("Failed to validate requested subscription cancellation for aspect '$aspect' on class $subject_class_name"); } # Return a ref to the callback removed. This is "true", but better than true. #return $found; last; } else { # Increment only if we did not splice-out a value. $index++; } } } $self->SUPER::delete(); } sub get_with_special_parameters { my($class,$rule,%extra) = @_; my $callback = delete $extra{'callback'}; if (keys %extra) { Carp::croak("Unrecognized parameters in get(): " . join(', ', keys(%extra))); } my @matches = $class->get($rule); return grep { $_->callback eq $callback } @matches; } 1; =pod =head1 NAME UR::Observer - bind callbacks to object changes =head1 SYNOPSIS $rocket = Acme::Rocket->create( fuel_level => 100 ); $observer = $rocket->add_observer( aspect => 'fuel_level', callback => sub { print "fuel level is: " . shift->fuel_level . "\n" }, priority => 2, ); $observer2 = UR::Observer->create( subject_class => 'Acme::Rocket', subject_id => $rocket->id, aspect => 'fuel_level', callback => sub { my($self,$changed_aspect,$old_value,$new_value) = @_; if ($new_value == 0) { print "Bail out!\n"; } }, priority => 0 ); for (3 .. 0) { $rocket->fuel_level($_); } # fuel level is: 3 # fuel level is: 2 # fuel level is: 1 # Bail out! # fuel level is: 0 $observer->delete; =head1 DESCRIPTION UR::Observer implements the observer pattern for UR objects. These observers can be attached to individual object instances, or to whole classes. They can send notifications for changes to object attributes, or to other state changes such as when an object is loaded from its datasource or deleted. =head1 CONSTRUCTOR Observers can be created either by using the method C on another class, or by calling C on the UR::Observer class. my $o1 = Some::Other::Class->add_observer(...); my $o2 = $object_instance->add_observer(...); my $o3 = UR::Observer->create(...); The constructor accepts these parameters: =over 2 =item subject_class_name The name of the class the observer is watching. If this observer is being created via C, then it figures out the subject_class_name from the class or object it is being called on. =item subject_id The ID of the object the observer is watching. If this observer is being created via C, then it figures out the subject_id from the object it was called on. If C was called as a class method, then subject_id is omitted, and means that the observer should fire for changes on any instance of the class or sub-class. =item priority A numeric value used to determine the order the callbacks are fired. Lower numbers are higher priority, and are run before callbacks with a numerically higher priority. The default priority is 1. Negative numbers are ok. =item aspect The attribute the observer is watching for changes on. The aspect is commonly one of the properties of the class. In this case, the callback is fired after the property's value changes. aspect can be omitted, which means the observer should fire for any change in the object state. If both subject_id and aspect are omitted, then the observer will fire for any change to any instance of the class. There are other, system-level aspects that can be watched for that correspond to other types of state change: =over 2 =item create After a new object instance is created =item delete After an n object instance is deleted =item load After an object instance is loaded from its data source =item commit After an object instance has changes saved to its data source =back =item callback A coderef that is called after the observer's event happens. The coderef is passed four parameters: $self, $aspect, $old_value, $new_value. In this case, $self is the object that is changing, not the UR::Observer instance (unless, of course, you have created an observer on UR::Observer). The return value of the callback is ignored. =item note A text string that is ignored by the system =back =head2 Custom aspects You can create an observer for an aspect that is neither a property nor one of the system aspects by listing the aspect names in the metadata for the class. class My::Class { has => [ 'prop_a', 'another_prop' ], valid_signals => ['custom', 'pow' ], }; my $o = My::Class->add_observer( aspect => 'pow', callback => sub { print "POW!\n" }, ); My::Class->__signal_observers__('pow'); # POW! my $obj = My::Class->create(prop_a => 1); $obj->__signal_observers__('custom'); # not an error To help catch typos, creating an observer for a non-standard aspect generates an error message but not an exception, unless the named aspect is in the list of 'valid_signals' in the class metadata. Nothing in the system will trigger these observers, but they can be triggered in your own code using the C<__signal_observers()__> class or object method. Sending a signal for an aspect that no observers are watching for is not an error. =cut Env000755023532023421 012121654175 13311 5ustar00abrummetgsc000000000000UR-0.41/lib/URUR_DBI_EXPLAIN_SQL_MATCH.pm000444023532023421 17212121654172 17600 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_MATCH; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_USED_MODS.pm000444023532023421 15612121654172 15753 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_USED_MODS; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DEBUG_OBJECT_PRUNING.pm000444023532023421 17112121654172 17444 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DEBUG_OBJECT_PRUNING; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_NO_COMMIT.pm000444023532023421 16212121654173 16431 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_NO_COMMIT; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_RUN_LONG_TESTS.pm000444023532023421 16312121654173 16635 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_RUN_LONG_TESTS; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_USE_DUMMY_AUTOGENERATED_IDS.pm000444023532023421 20012121654173 20535 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_USE_DUMMY_AUTOGENERATED_IDS; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_SUMMARIZE_SQL.pm000444023532023421 16612121654173 17144 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_SUMMARIZE_SQL; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_CONTEXT_CACHE_SIZE_HIGHWATER.pm000444023532023421 20112121654173 20624 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_CACHE_SIZE_HIGHWATER; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_SLOW.pm000444023532023421 17112121654173 17530 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_SLOW; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_MONITOR_DML.pm000444023532023421 16412121654174 16673 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_DML; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_MONITOR_SQL.pm000444023532023421 16412121654174 16716 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_SQL; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_IF.pm000444023532023421 16712121654174 17250 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_IF; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_NR_CPU.pm000444023532023421 15312121654174 15416 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_NR_CPU; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_STACK_DUMP_ON_WARN.pm000444023532023421 16712121654174 17312 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_STACK_DUMP_ON_WARN; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_EXPLAIN_SQL_CALLSTACK.pm000444023532023421 17612121654174 20253 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_EXPLAIN_SQL_CALLSTACK; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_MONITOR_EVERY_FETCH.pm000444023532023421 17412121654174 20063 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_MONITOR_EVERY_FETCH; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_NO_REQUIRE_USER_VERIFY.pm000444023532023421 17312121654174 20024 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_NO_REQUIRE_USER_VERIFY; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_CONTEXT_BASE.pm000444023532023421 16112121654174 16305 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_BASE; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DBI_DUMP_STACK_ON_CONNECT.pm000444023532023421 17612121654174 20312 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DBI_DUMP_STACK_ON_CONNECT; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_TEST_QUIET.pm000444023532023421 15712121654174 16122 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_TEST_QUIET; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_CONTEXT_ROOT.pm000444023532023421 16112121654174 16356 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_ROOT; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_CONTEXT_CACHE_SIZE_LOWWATER.pm000444023532023421 20012121654175 20547 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_CACHE_SIZE_LOWWATER; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_USED_LIBS.pm000444023532023421 16112121654175 15741 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_LIBS; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_IGNORE.pm000444023532023421 15312121654175 15354 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_IGNORE; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_STACK_DUMP_ON_DIE.pm000444023532023421 16612121654175 17144 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_STACK_DUMP_ON_DIE; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_DEBUG_OBJECT_RELEASE.pm000444023532023421 17112121654175 17405 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_DEBUG_OBJECT_RELEASE; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_COMMAND_DUMP_STATUS_MESSAGES.pm000444023532023421 20112121654175 20660 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_COMMAND_DUMP_STATUS_MESSAGES; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_TEST_FILLDB.pm000444023532023421 16012121654175 16162 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_TEST_FILLDB; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_USE_ANY.pm000444023532023421 15412121654175 15535 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_USE_ANY; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; UR_CONTEXT_MONITOR_QUERY.pm000444023532023421 17212121654175 17712 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Envpackage UR::Env::UR_CONTEXT_MONITOR_QUERY; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; 1; Value000755023532023421 012121654175 13635 5ustar00abrummetgsc000000000000UR-0.41/lib/URCSV.pm000444023532023421 31712121654172 14741 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::CSV; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::CSV', is => ['UR::Value'], ); 1; #$Header$ CODE.pm000444023532023421 34012121654172 15014 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::CODE; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::CODE', is => ['UR::Value::PerlReference'], ); 1; #$Header$ Decimal.pm000444023532023421 32312121654172 15641 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Decimal; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Decimal', is => ['UR::Value::Number'], ); 1; String.pm000444023532023421 33312121654172 15552 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::String; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::String', is => ['UR::Value::Text'], ); 1; #$Header$ FilePath.pm000444023532023421 53012121654172 15777 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::FilePath; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FilePath', is => ['UR::Value::FilesystemPath'], ); sub line_count { my $self = shift; my ($line_count) = qx(wc -l $self) =~ /^\s*(\d+)/; return $line_count; } 1; ARRAY.pm000444023532023421 34212121654172 15162 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::ARRAY; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::ARRAY', is => ['UR::Value::PerlReference'], ); 1; #$Header$ Text.pm000444023532023421 504212121654173 15253 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Text', is => ['UR::Value'], ); use overload ( '.' => \&concat, '""' => \&stringify, fallback => 1, ); sub swap { my ($a, $b) = @_; return ($b, $a); } sub concat { my ($self, $other, $swap) = @_; my $class = ref $self; $self = $self->id; ($self, $other) = swap($self, $other) if $swap; return $class->get($self . $other); } sub stringify { my $self = shift; return $self->id; } sub capitalize { my $self = shift; my $seps = join('', ' ', @_); # allow other separators my $regexp = qr/[$seps]+/; my $capitalized_string = join(' ', map { ucfirst } split($regexp, $self->id)); return $self->class->get($capitalized_string); } sub to_camel { my $self = shift; my $seps = join('', ( @_ ? @_ : ( ' ', '_' ))); my $regexp = qr/[$seps]+/; my $camel_case = join('', map { ucfirst } split($regexp, $self->id)); return $self->class->get($camel_case); } sub to_lemac { # camel backwards = undo camel case. This was nutters idea. Ignore 'git blame' my $self = shift; # Split on the first capital or the start of a number my @words = split( /(?=(?id); # Default join is a space my $join = ( defined $_[0] ) ? $_[0] : ' '; return $self->class->get( join($join, map { lc } @words) ); } sub to_hash { my ($self, $split) = @_; # split splits to value of a key into many values my $text = $self->id; if ( $text !~ m#^-# ) { Carp::cluck('Can not convert text object with id "' . $self->id . '" to hash. Text must start with a dash (-)'); return; } my %hash; my @values = split(/\s?(\-{1,2}\D[\w\d\-]*)\s?/, $text); shift @values; for ( my $i = 0; $i < @values; $i += 2 ) { my $key = $values[$i]; $key =~ s/^\-{1,2}//; if ( $key eq '' ) { Carp::cluck("Can not convert text ($text) to hash. Found empty dash (-)."); return; } my $value = $values[$i + 1]; if ( defined $value ){ $value =~ s/\s*$//; } else { $value = ''; } # FIXME What if the key exists? if ( defined $split ) { $hash{$key} = [ split($split, $value) ]; } else { $hash{$key} = $value; } } #print Data::Dumper::Dumper(\@values, \%hash); return UR::Value::HASH->get(\%hash); } 1; GLOB.pm000444023532023421 34012121654173 15026 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::GLOB; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::GLOB', is => ['UR::Value::PerlReference'], ); 1; #$Header$ DateTime.pm000444023532023421 33112121654173 15777 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::DateTime; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::DateTime', is => ['UR::Value'], ); 1; #$Header$ Timestamp.pm000444023532023421 34312121654173 16251 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Timestamp; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Timestamp', is => ['UR::Value::DateTime'], ); 1; #$Header$ SloppyPrimitive.pm000444023532023421 51712121654173 17470 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::SloppyPrimitive; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::SloppyPrimitive', is => ['UR::Value'], ); # namespaces which have allow_sloppy_primitives() set to true # will use this for any unrecognizable data types. 1; URL.pm000444023532023421 32512121654174 14751 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::URL; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::URL', is => ['UR::Value::Text'], ); 1; #$Header$ HASH.pm000444023532023421 236312121654174 15056 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::HASH; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::HASH', is => ['UR::Value::PerlReference'], ); sub __display_name__ { my $self = shift; my $hash = $self->id; my @values; for my $key (sort keys %$hash) { next unless defined $hash->{$key}; push @values, "$key => '".( defined $hash->{$key} ? $hash->{$key} : '' ). "'"; } my $join = ( defined $_[0] ) ? $_[0] : ','; # Default join is a comma return join($join, @values); } sub to_text { my $self = shift; my $hash = $self->id; my @tokens; for my $key (sort keys %$hash) { push @tokens, '-'.$key; next if not defined $hash->{$key} or $hash->{$key} eq ''; if ( my $ref = ref $hash->{$key} ) { if ( $ref ne 'ARRAY' ) { Carp::cluck("Can not convert hash to text. Cannot handle $ref for $key"); return; } push @tokens, @{$hash->{$key}}; } else { push @tokens, $hash->{$key}; } } my $join = ( defined $_[0] ) ? $_[0] : ' '; # Default join is a space return UR::Value::Text->get( join($join, @tokens)); } 1; Float.pm000444023532023421 31712121654174 15355 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Float; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Float', is => ['UR::Value::Number'], ); 1; Blob.pm000444023532023421 32112121654174 15161 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Blob; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Blob', is => ['UR::Value'], ); 1; #$Header$ Integer.pm000444023532023421 34012121654174 15701 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Integer; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Integer', is => ['UR::Value::Number'], ); 1; #$Header$ DirectoryPath.pm000444023532023421 35512121654174 17073 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::DirectoryPath; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::DirectoryPath', is => ['UR::Value::FilePath'], ); 1; #$Header$ FilesystemPath.pm000444023532023421 63212121654174 17251 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::FilesystemPath; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FilesystemPath', is => 'UR::Value::Text', ); sub exists { return -e shift; } sub is_dir { return -d shift; } sub is_file { return -f shift; } sub is_symlink { return -l shift; } sub size { return -s shift; } 1; Set.pm000444023532023421 47012121654175 15044 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Set; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub members { my $self = shift; my %params = $self->rule->params_list; my $id = $params{id}; if (ref($id) eq 'ARRAY') { return (@$id); } else { return ($id); } } 1; FOF.pm000444023532023421 31712121654175 14723 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::FOF; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::FOF', is => ['UR::Value'], ); 1; #$Header$ SCALAR.pm000444023532023421 34412121654175 15256 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::SCALAR; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::SCALAR', is => ['UR::Value::PerlReference'], ); 1; #$Header$ REF.pm000444023532023421 33612121654175 14726 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::REF; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::REF', is => ['UR::Value::PerlReference'], ); 1; #$Header$ PerlReference.pm000444023532023421 114612121654175 17053 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::PerlReference; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::PerlReference', is => ['UR::Value'], ); my %underlying_data_types; sub underlying_data_types { my $class = shift; my $class_name = ref($class) ? $class->class_name : $class; unless (exists $underlying_data_types{$class_name}) { my($base_type) = ($class_name =~ m/^UR::Value::(.*)/); $underlying_data_types{$class_name} = [$base_type]; } return @{$underlying_data_types{$class_name}}; } 1; #$Header$ Number.pm000444023532023421 31112121654175 15533 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Number; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Number', is => ['UR::Value'], ); 1; Boolean.pm000444023532023421 32212121654175 15664 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Boolean; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Value::Boolean', is => ['UR::Value::Text'], ); 1; Iterator.pm000444023532023421 67412121654175 16110 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Valuepackage UR::Value::Iterator; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub create { my $class = shift; my $set = $class->define_set(@_); my @members = $set->members; return $class->create_for_value_arrayref(\@members); } sub create_for_value_arrayref { my ($class, $arrayref) = @_; return bless { members => $arrayref }, $class; } sub next { shift @{ shift->{members} }; } 1; View000755023532023421 012121654172 14544 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ValueDefault000755023532023421 012121654175 16133 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/ViewXml.t000444023532023421 765112121654172 17223 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/View/Default#!/usr/bin/env perl use strict; use warnings; use Test::More; use above "UR"; # Test PerlReference UR::Value Views test_SCALAR_view(); test_ARRAY_view(); test_HASH_view(); done_testing(); sub test_SCALAR_view { my $view = UR::Value::View::Default::Xml->create(); isa_ok($view, 'UR::Object::View', 'view'); my $string = 'abc'; my $scalar = \$string; my $scalar_object = UR::Value::SCALAR->get($scalar); isa_ok($scalar_object, 'UR::Value', 'scalar_object'); $view->subject($scalar_object); my $xml = $view->content; $xml =~ tr/\n//d; # remove newlines $xml =~ s/>\s+', '', 'SCALAR()', 'SCALA R', '', '', '', '', '', '', '', 'abc', '', '', ); is($xml, $expected_xml, 'SCALAR xml matches expected output'); } sub test_ARRAY_view { my $view = UR::Value::View::Default::Xml->create(); isa_ok($view, 'UR::Object::View', 'view'); my $array = [ 1, 2, 4 ]; my $array_object = UR::Value::ARRAY->get($array); isa_ok($array_object, 'UR::Value', 'array_object'); $view->subject($array_object); my $xml = $view->content; $xml =~ tr/\n//d; # remove newlines $xml =~ s/>\s+', '', 'ARRAY()', 'ARRA Y', '', '', '', '', '', '', '', '', '1', '2', '4', '', '', '', ); is($xml, $expected_xml, 'ARRAY xml matches expected output'); } sub test_HASH_view { my $view = UR::Value::View::Default::Xml->create(); isa_ok($view, 'UR::Object::View', 'view'); my $hash = { a => 1, b => 2, c => 4 }; my $hash_object = UR::Value::HASH->get($hash); isa_ok($hash_object, 'UR::Value', 'hash_object'); $view->subject($hash_object); my $xml = $view->content; $xml =~ tr/\n//d; # remove newlines $xml =~ s/>\s+', '', 'HASH()', 'HAS H', '', '', '', '', '', '', '', '', '1', '2', '4', '', '', '', ); is($xml, $expected_xml, 'HASH xml matches expected output'); } Xml.pm000444023532023421 144612121654173 17371 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Xml; use strict; use warnings; use UR; class UR::Value::View::Default::Xml { is => ['UR::Object::View::Default::Xml', 'UR::Value::View::Default::Text'], }; sub _generate_xml_doc { my $self = shift; my $xml_doc = $self->SUPER::_generate_xml_doc(@_); if ($self->subject_class_name->isa('UR::Value::PerlReference')) { $self->_add_perl_data_to_node($self->subject_id); } return $xml_doc; } sub _generate_content { my $self = shift; my $content; if ($self->subject_class_name->isa('UR::Value::PerlReference')) { $content = $self->UR::Object::View::Default::Xml::_generate_content(@_); } else { $content = $self->UR::Value::View::Default::Text::_generate_content(@_); } return $content; } 1; Html.pm000444023532023421 24512121654173 17511 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Html; use strict; use warnings; use UR; class UR::Value::View::Default::Html { is => 'UR::Value::View::Default::Text', }; 1; Json.pm000444023532023421 37212121654174 17520 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Json; use strict; use warnings; use UR; # These Values inherit from Text which inherits from UR::Object::View::Default::Text class UR::Value::View::Default::Json { is => 'UR::Value::View::Default::Text', }; 1; Text.pm000444023532023421 52412121654175 17533 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/View/Defaultpackage UR::Value::View::Default::Text; use strict; use warnings; use UR; class UR::Value::View::Default::Text { is => 'UR::Object::View::Default::Text', }; sub _generate_content { my $self = shift; my $subject = $self->subject; return unless ($subject); my $name = $subject->__display_name__; return $name; } 1; Boolean000755023532023421 012121654173 15212 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ValueView000755023532023421 012121654173 16124 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/BooleanDefault000755023532023421 012121654173 17510 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/Boolean/ViewText.pm000444023532023421 54112121654173 21107 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Value/Boolean/View/Defaultpackage UR::Value::Boolean::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Value::Boolean::View::Default::Text { is => 'UR::Object::View::Default::Text' }; sub _generate_content { my $self = shift; my $subject = $self->subject(); return $subject && $subject->id ? "1" : "0"; } 1; Object000755023532023421 012121654175 13767 5ustar00abrummetgsc000000000000UR-0.41/lib/URSet.pm000444023532023421 2605512121654172 15242 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Set; use strict; use warnings; use UR; use List::MoreUtils qw(any); our $VERSION = "0.41"; # UR $VERSION; our @CARP_NOT = qw( UR::Object::Type ); use overload ('""' => '__display_name__'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('!=' => sub { $_[0] . '' ne $_[1] . '' } ); use overload ('ne' => sub { $_[0] . '' ne $_[1] . '' } ); class UR::Object::Set { is => 'UR::Value', is_abstract => 1, has => [ rule => { is => 'UR::BoolExpr', id_by => 'id' }, rule_display => { is => 'Text', via => 'rule', to => '__display_name__'}, member_class_name => { is => 'Text', via => 'rule', to => 'subject_class_name' }, members => { is => 'UR::Object', is_many => 1, is_calculated => 1 } ], doc => 'an unordered group of distinct UR::Objects' }; # override the UR/system display name # this is used in stringification overload sub __display_name__ { my $self = shift; my %b = $self->rule->_params_list; my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Dump; $s =~ s/\n/ /gs; $s =~ s/^\s*{//; $s =~ s/\}\s*$//; $s =~ s/\"(\w+)\" \=\> / $1 => /g; return '(' . ref($self) . ' ' . $s . ')'; } # I'll neave this in here commented out for the future # It's intended to keep 'count' for sets updated in real-time as objects are # created/deleted/updated #sub _load { # my $class = shift; # my $self = $class->SUPER::_load(@_); # # my $member_class_name = $rule->subject_class_name; # # my $rule = $self->rule # my $rule_template = $rule->template; # # my @rule_properties = $rule_template->_property_names; # my %rule_values = map { $_ => $rule->value_for($_) } @rule_properties; # # my %underlying_comparator_for_property = map { $_->property_name => $_ } $rule_template->get_underlying_rule_templates; # # my @aggregates = qw( count ); # # $member_class_name->create_subscription( # note => 'set monitor '.$self->id, # priority => 0, # callback => sub { # # make sure the aggregate values get invalidated when objects change # my @agg_set = @$self{aggregates}; # return unless exists(@agg_set); # returns only if none of the aggregates have values # # my ($changed_object, $changed_property, $old_value, $new_value) = @_; # # if ($changed_property eq 'create') { # if ($rule->evaluate($changed_object)) { # $self->{'count'}++; # } # } elsif ($changed_property eq 'delete') { # if ($rule->evaluate($changed_object)) { # $self->{'count'}--; # } # } elsif (exists $value_index_for_property{$changed_property}) { # # my $comparator = $underlying_comparator_for_property{$changed_property}; # # # HACK! # $changed_object->{$changed_property} = $old_value; # my $evaled_before = $comparator->evaluate_subject_and_values($changed_object,$rule_values{$changed_property}); # # $changed_object->{$changed_property} = $new_value; # my $evaled_after = $comparator->evaluate_subject_and_values($changed_object,$rule_values{$changed_property}); # # if ($evaled_before and ! $evaled_after) { # $self->{'count'}--; # } elsif ($evaled_after and ! $evaled_before) { # $self->{'count'}++; # } # } # } # ); # # return $self; #} sub get_with_special_parameters { Carp::cluck("Getting sets by directly properties of their members method will be removed shortly because of ambiguity on the meaning of 'id'. Please update the code which calls this."); my $class = shift; my $bx = shift; my @params = @_; my $member_class = $class; $member_class =~ s/::Set$//; my $rule = UR::BoolExpr->resolve($member_class, $bx->params_list, @params); return $class->get($rule->id); } sub members { my $self = shift; my $rule = $self->rule; while (@_) { $rule = $rule->add_filter(shift, shift); } return $self->member_class_name->get($rule); } sub _members_have_changes { my $self = shift; return any { $self->rule->evaluate($_) && $_->__changes__ } $self->member_class_name->is_loaded; } sub subset { my $self = shift; my $member_class_name = $self->member_class_name; my $bx = UR::BoolExpr->resolve($member_class_name,@_); my $subset = $self->class->get($bx->id); return $subset; } sub group_by { my $self = shift; my @group_by = @_; my $grouping_rule = $self->rule->add_filter(-group_by => \@group_by); my @groups = UR::Context->current->get_objects_for_class_and_rule( $self->member_class_name, $grouping_rule, undef, #$load, 0, #$return_closure, ); return $self->context_return(@groups); } sub __aggregate__ { my $self = shift; my $f = shift; Carp::croak("$f is a group operation, and is not writable") if @_; my $subject_class_meta = $self->rule->subject_class_name->__meta__; my $not_ds_expressable = grep { $_->is_calculated or $_->is_transient or $_->is_constant } map { $_->final_property_meta or $_ } map { $subject_class_meta->property_meta_for_name($_) || () } $self->rule->template->_property_names; # If there are no member-class objects with changes, we can just interrogate the DB if ($self->_members_have_changes or $not_ds_expressable) { my $fname; my @fargs; if ($f =~ /^(\w+)\((.*)\)$/) { $fname = $1; @fargs = ($2 ? split(',',$2) : ()); } else { $fname = $f; @fargs = (); } my $local_method = '__aggregate_' . $fname . '__'; $self->{$f} = $self->$local_method(@fargs); } elsif (! exists $self->{$f}) { my $rule = $self->rule->add_filter(-aggregate => [$f])->add_filter(-group_by => []); UR::Context->current->get_objects_for_class_and_rule( $self->member_class_name, $rule, 1, # load 0, # return_closure ); } return $self->{$f}; } sub __aggregate_count__ { my $self = shift; my @members = $self->members; return scalar(@members); } sub __aggregate_min__ { my $self = shift; my $p = shift; my $min = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $min = $v if not defined $min or $v < $min; } return $min; } sub __aggregate_max__ { my $self = shift; my $p = shift; my $max = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $max = $v if not defined $max or $v > $max; } return $max; } sub __aggregate_sum__ { my $self = shift; my $p = shift; my $sum = undef; no warnings; for my $member ($self->members) { my $v = $member->$p; next unless defined $v; $sum += $v; } return $sum; } sub __related_set__ { my $self = $_[0]; my $property_name = $_[1]; my $bx1 = $self->rule; my $bx2 = $bx1->reframe($property_name); return $bx2->subject_class_name->define_set($bx2); } require Class::AutoloadCAN; Class::AutoloadCAN->import(); sub CAN { my ($class,$method,$self) = @_; if ($method =~ /^__aggregate_(.*)__/) { # prevent circularity issues since this actually calls ->can(); return; } my $member_class_name = $class; $member_class_name =~ s/::Set$//g; return unless $member_class_name; my $is_class_method = !ref($self); my $member_method_closure = $member_class_name->can($method); if ($is_class_method && $member_method_closure) { # We should only get here if the Set class has not implemented the method. # In which case we will delegate to the member class. return sub { my $self = shift; return $member_method_closure->($member_class_name, @_); }; } if ($member_method_closure) { my $member_class_meta = $member_class_name->__meta__; my $member_property_meta = $member_class_meta->property_meta_for_name($method); # regular property access if ($member_property_meta) { return sub { my $self = shift; if (@_) { Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable"); } my $rule = $self->rule; if ($rule->specifies_value_for($method)) { return $rule->value_for($method); } else { my @members = $self->members; my @values = map { $_->$method } @members; return @values if wantarray; return if not defined wantarray; Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray; return $values[0]; } }; } # set relaying with $s->foo_set->bar_set->baz_set; if (my ($property_name) = ($method =~ /^(.*)_set$/)) { return sub { shift->__related_set__($property_name, @_) } } # other method return sub { my $self = shift; if (@_) { Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable"); } my @members = $self->members; my @values = map { $_->$method } @members; return @values if wantarray; return if not defined wantarray; Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray; return $values[0]; }; } else { # a possible aggregation function # see if the method ___aggregate__ uses exists, and if so, delegate to __aggregate__ # TODO: delegate these to aggregation function modules instead of having them in this module my $aggregator = '__aggregate_' . $method . '__'; if ($self->can($aggregator)) { return sub { my $self = shift; my $f = $method; if (@_) { $f .= '(' . join(',',@_) . ')'; } return $self->__aggregate__($f); }; } # set relaying with $s->foo_set->bar_set->baz_set; if (my ($property_name) = ($method =~ /^(.*)_set$/)) { return sub { shift->__related_set__($property_name, @_) } } } return; } 1; Join.pm000444023532023421 4071312121654172 15403 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Join; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; our @CARP_NOT = qw( UR::Object::Property ); class UR::Object::Join { #is => 'UR::Value', id_by => [ id => { is => 'Text' }, ], has_optional_transient => [ source_class => { is => 'Text' }, source_property_names => { is => 'Text' }, foreign_class => { is => 'Text' }, foreign_property_names => { is => 'Text' }, source_name_for_foreign => { is => 'Text' }, foreign_name_for_source => { is => 'Text' }, is_optional => { is => 'Boolean' }, is_many => { is => 'Boolean' }, sub_group_label => { is => 'Text' }, where => { is => 'Text' }, ], doc => "join metadata used internally by the ::QueryBuilder" }; our %resolve_chain; sub resolve_chain { my ($class, $class_name, $property_chain, $join_label) = @_; $join_label ||= $property_chain; my $join_chain = $resolve_chain{$class_name}{$join_label}{$property_chain} ||= do { my $class_meta = $class_name->__meta__; my @pmeta = $class_meta->property_meta_for_name($property_chain); my @joins; for my $pmeta (@pmeta) { push @joins, $class->_resolve_chain_for_property_meta($pmeta,$join_label); } \@joins; }; return @$join_chain; } sub _resolve_chain_for_property_meta { my ($class, $pmeta, $join_label) = @_; if ($pmeta->via or $pmeta->to) { return $class->_resolve_via_to($pmeta,$join_label); } else { my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { return; } if ($pmeta->id_by or $foreign_class->isa("UR::Value")) { return $class->_resolve_forward($pmeta, $join_label); } elsif (my $reverse_as = $pmeta->reverse_as) { return $class->_resolve_reverse($pmeta,$join_label); } else { # TODO: handle hard-references to objects here maybe? $pmeta->error_message("Property '" . $pmeta->property_name . "' of class " . $pmeta->class_name . " has no 'id_by' or 'reverse_as' property metadata"); return; } } } sub _get_or_define { my $class = shift; my %p = @_; my $id = delete $p{id}; delete $p{__get_serial}; delete $p{db_committed}; delete $p{_change_count}; delete $p{__defined}; my $self = $class->get(id => $id); unless ($self) { $self = $class->__define__($id); for my $k (keys %p) { $self->$k($p{$k}); no warnings; unless ($self->{$k} eq $p{$k}) { Carp::confess(Data::Dumper::Dumper($self, \%p)); } } } unless ($self) { Carp::confess("Failed to create join???"); } return $self; } sub _resolve_via_to { my ($class, $pmeta,$join_label) = @_; my $class_name = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $class_name); my @joins; my $via = $pmeta->via; my $to = $pmeta->to; if ($via and not $to) { $to = $pmeta->property_name; } my $via_meta; if ($via) { if ($via eq '__self__') { my $to_meta = $class_meta->property_meta_for_name($to); unless ($to_meta) { my $property_name = $pmeta->property_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata 'to' property '$to'"; } return $to_meta->_resolve_join_chain($join_label); } $via_meta = $class_meta->property_meta_for_name($via); unless ($via_meta) { return if $class_name->can($via); # It's via a method, not an actual property my $property_name = $pmeta->property_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata for via property '$via'"; } if ($via_meta->to and ($via_meta->to eq '-filter')) { return $via_meta->_resolve_join_chain($join_label); } unless ($via_meta->data_type) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No data type for via property '$via'"; } push @joins, $via_meta->_resolve_join_chain($join_label); if (my $where = $pmeta->where) { my $join = pop @joins; unless ($join and $join->{foreign_class}) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak("Can't resolve joins for property '$property_name' of $class_name: Couldn't determine foreign class for via property '$via'\n" . "join data so far: ". Data::Dumper::Dumper($join, \@joins)); } my $where_rule = UR::BoolExpr->resolve($join->{foreign_class}, @$where); my $id = $join->{id}; $id .= ' ' . $where_rule->id . ":$join_label"; my %join_data = %$join; push @joins, $class->_get_or_define(%join_data, id => $id, where => $where, sub_group_label => $pmeta->property_name); } } else { $via_meta = $pmeta; } if ($to and $to ne '__self__' and $to ne '-filter') { my $to_class_meta = eval { $via_meta->data_type->__meta__ }; unless ($to_class_meta) { Carp::croak("Can't get class metadata for " . $via_meta->data_type . " while resolving property '" . $pmeta->property_name . "' in class " . $pmeta->class_name . "\n" . "Is the data_type for property '" . $via_meta->property_name . "' in class " . $via_meta->class_name . " correct?"); } my $to_meta = $to_class_meta->property_meta_for_name($to); unless ($to_meta) { my $property_name = $pmeta->property_name; my $class_name = $pmeta->class_name; Carp::croak "Can't resolve property '$property_name' of $class_name: No '$to' property found on " . $via_meta->data_type; } push @joins, $to_meta->_resolve_join_chain($join_label); } if (my $return_class_name = $pmeta->_convert_data_type_for_source_class_to_final_class($pmeta->data_type, $pmeta->class_name)) { my $final_class_name = $joins[-1]->foreign_class; if ($return_class_name ne $final_class_name) { if ($return_class_name->isa($final_class_name)) { # the property is a subclass of the one involved in the final join # this happens when there is a via/where/to where say "to" goes-to any "Animal" but this overall property is known to be a "Dog". my $general_join = pop @joins; my $specific_join = UR::Object::Join->_get_or_define( source_class => $general_join->{'source_class'}, source_property_names => $general_join->{'source_property_names'}, foreign_class => $return_class_name, # more specific foreign_property_names => $general_join->{'foreign_property_names'}, # presume the borrow took you into a subclass and these still work is_optional => $general_join->{'is_optional'}, id => $general_join->{id} . ' isa ' . $return_class_name ); push @joins, $specific_join; } elsif ($return_class_name eq 'UR::Value::SloppyPrimitive' or $final_class_name eq 'UR::Value::SloppyPrimitive') { # backward-compatible layer for before there were primitive types } elsif ($final_class_name->isa($return_class_name)) { Carp::carp("Joins for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name . " is declared as data type $return_class_name while its joins connect to a more specific data type $final_class_name!"); } else { #Carp::carp("Discrepant join for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name # . ". Its data type ($return_class_name) does not match the join from property '" # . join("','", @{$joins[-1]->{source_property_names}}) . "' of class " . $joins[-1]->{source_class} # . " with type $final_class_name"); } } } return @joins; } # code below uses these to convert objects using hash slices my @old = qw/source_class source_property_names foreign_class foreign_property_names source_name_for_foreign foreign_name_for_source is_optional is_many sub_group_label/; my @new = qw/foreign_class foreign_property_names source_class source_property_names foreign_name_for_source source_name_for_foreign is_optional is_many sub_group_label/; sub _resolve_forward { my ($class, $pmeta, $join_label) = @_; my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { #Carp::cluck("No metadata?!"); return; } my $source_class = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name); my @joins; my $where = $pmeta->where; my $foreign_class_meta = $foreign_class->__meta__; my $property_name = $pmeta->property_name; my $id = $source_class . '::' . $property_name; if ($where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } ##### # direct reference (or primitive, which is a direct ref to a value obj) my (@source_property_names, @source_property_types, @foreign_property_names, @foreign_property_types, $source_name_for_foreign, $foreign_name_for_source); if ($foreign_class->isa("UR::Value")) { if (my $id_by = $pmeta->id_by) { my @id_by = ref($id_by) eq 'ARRAY' ? @$id_by : ($id_by); foreach my $id_by_name ( @id_by ) { my $id_by_property = $class_meta->property_meta_for_name($id_by_name); push @joins, $id_by_property->_resolve_join_chain($join_label); } } @source_property_names = ($property_name); @foreign_property_names = ('id'); $source_name_for_foreign = ($property_name); } elsif (my $id_by = $pmeta->id_by) { my @pairs = $pmeta->get_property_name_pairs_for_join; @source_property_names = map { $_->[0] } @pairs; @foreign_property_names = map { $_->[1] } @pairs; if (ref($id_by) eq 'ARRAY') { # satisfying the id_by requires joins of its own # sms: why is this only done on multi-value fks? foreach my $id_by_property_name ( @$id_by ) { my $id_by_property = $class_meta->property_meta_for_name($id_by_property_name); next unless ($id_by_property and $id_by_property->is_delegated); push @joins, $id_by_property->_resolve_join_chain($join_label); $source_class = $joins[-1]->{'foreign_class'}; @source_property_names = @{$joins[-1]->{'foreign_property_names'}}; } } $source_name_for_foreign = $pmeta->property_name; my @reverse = $foreign_class_meta->properties(reverse_as => $source_name_for_foreign, data_type => $pmeta->class_name); my $reverse; if (@reverse > 1) { my @reduced = grep { not $_->where } @reverse; if (@reduced != 1) { Carp::confess("Ambiguous results finding reversal for $property_name!" . Data::Dumper::Dumper(\@reverse)); } $reverse = $reduced[0]; } else { $reverse = $reverse[0]; } if ($reverse) { $foreign_name_for_source = $reverse->property_name; } } # the foreign class might NOT have a reverse_as, but # this records what to reverse in this case. $foreign_name_for_source ||= '<' . $source_class . '::' . $source_name_for_foreign; $id .= ":$join_label"; push @joins, $class->_get_or_define( id => $id, source_class => $source_class, source_property_names => \@source_property_names, foreign_class => $foreign_class, foreign_property_names => \@foreign_property_names, source_name_for_foreign => $source_name_for_foreign, foreign_name_for_source => $foreign_name_for_source, is_optional => ($pmeta->is_optional or $pmeta->is_many), is_many => $pmeta->is_many, where => $where, ); return @joins; } sub _resolve_reverse { my ($class, $pmeta,$join_label) = @_; my $foreign_class = $pmeta->_data_type_as_class_name; unless (defined($foreign_class) and $foreign_class->can('get')) { #Carp::cluck("No metadata?!"); return; } my $source_class = $pmeta->class_name; my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name); my @joins; my $where = $pmeta->where; my $property_name = $pmeta->property_name; my $id = $source_class . '::' . $property_name; if ($where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } ##### my $reverse_as = $pmeta->reverse_as; my $foreign_class_meta = $foreign_class->__meta__; my $foreign_property_via = $foreign_class_meta->property_meta_for_name($reverse_as); unless ($foreign_property_via) { Carp::confess("No property '$reverse_as' in class $foreign_class, needed to resolve property '" . $pmeta->property_name . "' of class " . $pmeta->class_name); } my @join_data = map { { %$_ } } $foreign_property_via->_resolve_join_chain($join_label); my $prev_where = $where; for (@join_data) { @$_{@new} = @$_{@old}; my $next_where = $_->{where}; $_->{where} = $prev_where; no warnings qw(uninitialized); #source_name_for_foreign can be undefined at the end of the chain my $id = $_->{source_class} . '::' . $_->{source_name_for_foreign}; use warnings qw(uninitialized); if ($prev_where) { my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where); $id .= ' ' . $where_rule->id; } $id .= ":$join_label"; $_->{id} = $id; $_->{is_optional} = ($pmeta->is_optional || $pmeta->is_many); $_->{is_many} = $pmeta->{is_many}; $_->{sub_group_label} = $pmeta->property_name; $prev_where = $next_where; } @join_data = reverse @join_data; if ($prev_where) { # Having a where clause in the last join is only a problem if testing # the where condition needs more joins. But if it did, then those additional # joins would have already been in the list, right? #Carp::confess("final join needs placement! " . Data::Dumper::Dumper($prev_where)); } for my $join_data (@join_data) { push @joins, $class->_get_or_define(%$join_data); } return @joins; } # Return true if the foreign-end of the join includes all the ID properties of # the foreign class. Used by the ObjectFabricator when it is determining whether or # not to include more rules in the all_params_loaded hash for delegations sub destination_is_all_id_properties { my $self = shift; my $foreign_class_meta = $self->{'foreign_class'}->__meta__; my %join_properties = map { $_ => 1 } @{$self->{'foreign_property_names'}}; my $join_has_all_id_props = 1; foreach my $foreign_id_meta ( $foreign_class_meta->all_id_property_metas ) { next if $foreign_id_meta->class_name eq 'UR::Object'; # Skip the manufactured 'id' property next if (delete $join_properties{ $foreign_id_meta->property_name }); $join_has_all_id_props = 0; } return $join_has_all_id_props; } 1; Type.pod000444023532023421 3051112121654173 15567 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object=pod =head1 NAME UR::Object::Type - a meta-class for any class or primitive type =head1 SYNOPSIS use UR; class MyClass { is => ['ParentClass1', 'ParentClass2'], id_by => [ id_prop1 => { is => 'Integer' }, id_prop2 => { is => 'String' }, ], has => [ property_a => { is => 'String' } property_b => { is => 'Integer', is_optional => 1 }, ], }; my $meta = MyClass->__meta__; my @parent_class_metas = $meta->parents(); # 2 meta objects, see UR::Object::Property my @property_meta = $meta->properties(); # N properties (4, +1 from UR::Object, +? from ParentClass1 and ParentClass2) $meta->is_abstract; $meta->... =head1 DESCRIPTION UR::Object::Type implements the class behind the central metadata in the UR class framework. It contains methods for introspection and manipulation of related class data. A UR::Object::Type object describes UR::Object, and also every subclass of UR::Object. =head1 INHERITANCE In addition to describing UR::Object an each of its subclasses, UR::Object::Type is _itself_ a subclass of L. This means that the same query APIs used for regular objects can be used for meta objects. UR::Object -> has-meta -> UR::Object::Type A | \ / \-----<- is-a <-------/ Further, new classes which generate a new UR::Objec::Type, also generate a new subclass for the meta-class. This means that each new class can have private meta methods, (ala Ruby). This means that extensions to a meta-class, apply to the meta-class of its derivatives. Regular Meta-Class Entity Singleton ------- ---------- Greyhound has-meta -> Greyhound::Type | | V V is-a is-a | | V V Dog has-meta -> Dog::Type | | V V is-a is-a | | V V Animal has-meta -> Animal::Type | | V V is-a is-a | | /-----------------\ V V V | UR::Object has-meta -> UR::Object::Type has-meta -/ A is-a | | \______________________/ =head1 CONSTRUCTORS =over 4 =item "class" class MyClass1 {}; class MyClass2 { is => 'MyClass1' }; class MyClass3 { is => ['Parent1','Parent2'], is_abstract => 1, is_transient => 1, has => [ qw/p1 p2 p3/ ], doc => 'woo hoo!' }; The primary constructor is not a method on this class at all. UR catches "class SOMENAME { ... }" and calls define() with the parameters. =item define my $class_obj = UR::Object::Type->define( class_name => 'MyClass', ... ); Register a class with the system. The given class_name must be unique within the application. As a side effect, a new Perl namespace will be created for the class's name, and methods will be injected into that namespace for any of the class properties. Other types of metadata objects will get created to manage the properties and relationships to other classes. See the L documentation for more information about the parameters C accepts. =item create my $class_obj = UR::Object::Type->create( class_name => 'Namespace::MyClass', ... ); Create a brand new class within an already existing UR namespace. C takes all the same parameters as C. Another side effect of create is that when the application commits its Context, a new Perl module will be created to implement the class, complete with a class definition. Applications will not normally use create(). =back =head1 PROPERTIES Each property has a method of the same name =head2 External API =over 4 =item class_name $name = $class_obj->class_name The name of the class. Class names are unique within a UR namespace and an application. This is symmetrical with $class_obj = $name->__meta__. =item properties @all = $class_obj->properties(); @some = $class_obj->properties( 'is => ['Text','Number'] 'doc like' => '%important%', 'property_name like' => 'someprefix_%', ); Access the related property meta-objects for all properties of this class. It includes the properties of any parent classes which are inherited by this class. See L for details. =item property $property_meta = $class_obj->property('someproperty'); The singular version of the above. A single argument, as usual, is treated as the remainder of the ID, and will select a property by name. =item namespace $namespace_name = $class_obj->namespace Returns the name of the class's UR namespace. =item doc $doc = $class_obj->doc A place to put general class-specific notes. =item data_source_id $ds_id = $class_obj->data_source_id The name of the external data source behind this class. Classes without data sources cannot be saved and exist only during the life of the application. data_source_id will resolve to an L id. =item table_name $table_name = $class_object->table_name For classes with data sources, this is the name of the table within that data source. This is usually a table in a relational database. At a basic level, it is a storage directive interpreted by the data_source, and may or may not related to a storage table at that level. =item is_abstract $bool = $class_obj->is_abstract A flag indicating if this is an abstract class. Abstract classes cannot have instances, but can be inherited by other classes. =item is_final $bool = $class_obj->is_final A flag indicating if this class cannot have subclasses. =item is_singleton $bool = $class_obj->is_singleton A flag indicating whether this is a singleton class. If true, the class will inherit from L. =item is_transactional $bool = $class_obj->is_transactional A flag indicating whether changes to this class's instances will be tracked. Non-transactional objecs do not change when an in-memory transaction rolls back. It is similar to the is_transient meta-property, which does the same for an individual property. =back =head2 Internal API These methods return data about how this class relates to other classes. =over 4 =item namespace_meta $ns_meta = $class_obj->namespace_meta Returns the L object with the class's namespace name. =item parent_class_names @names = $class_obj->parent_class_names Returns a list of the immediate parent classes. =item parent_class_metas @class_objs = $class_obj->parent_class_metas Returns a list of the class objects (L instances) of the immediate parent classes =item ancestry_class_names @names = $class_obj->ancestry_class_names Returns a list of all the class names this class inherits from, directly or indirectly. This list may have duplicate names if there is multiple inheritance in the family tree. =item ancestry_class_metas @class_objs = $class_obj->ancestry_class_metas Returns a list of the class objects for each inherited class. =item direct_property_names @names = $class_obj->direct_property_names Returns a list of the property names defined within this class. This list will not include the names of any properties inherited from parent classes unless they have been overridden. =item direct_property_metas @property_objs = $class_obj->direct_property_metas Returns a list of the L objects for each direct property name. =item ancestry_property_names @names = $class_obj->ancestry_property_names Returns a list of property names of the parent classes and their inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item ancestry_property_metas @property_objs = $class_obj->ancestry_property_metas; Returns a list of the L objects for each ancestry property name. =item all_property_names Returns a list of property names of the given class and its inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item all_property_metas @property_objs = $class_obj->all_property_metas; Returns a list of the L objects for each name returned by all_property_names. =item direct_id_property_names @names = $class_obj->direct_id_property_names Returns a list of the property names designated as "id" properties in the class definition. =item direct_id_property_metas @property_objs = $class_obj->direct_id_property_metas Returns a list of the L objects for each id property name. =item ancestry_id_property_names =item ancestry_id_property_metas =item all_id_property_names =item all_id_property_metas @names = $class_obj->ancestry_id_property_names; @property_objs = $class_obj->ancestry_id_property_metas; @names = $class_obj->all_id_property_names; @property_objs = $class_obj->all_id_property_metas; Returns the property names or L objects for either the parent classes and their inheritance heirarchy, or for the given class and all of its inheritance heirarchy. The lists may include duplicates if properties are overridden somewhere in the heirarchy. =item unique_property_set_hashref $constraints = $class_obj->unique_property_set_hashref Return a hashref describing the unique constraints on the given class. The keys of $constraint are constraint names, and the values are listrefs of property names that make up the unique constraint. =item add_unique_constraint $class_obj->add_unique_constraint($constraint_name, @property_name_list) Add a unique constraint to the given class. It is an exception if the given $constraint_name already exists as a constraint on this class or its parent classes. =item remove_unique_constraint $class_obj->remove_unique_constraint($constraint_name) Remove a unique constraint from the given class. It is an exception if the given constraint name does not exist. =item ancestry_table_names =item all_table_names @names = $class_obj->ancestry_table_names Returns a list of table names in the class's inheritance heirarchy. =item direct_column_names Returns a list of column names for each direct property meta. Classes with data sources and table names will have properties with column names. =item direct_id_column_names Returns a list of ID column names for each direct property meta. =item direct_columnless_property_names =item direct_columnless_property_metas =item ancestry_columnless_property_names =item ancestry_columnless_property_metas =item all_columnless_property_names =item all_columnless_property_metas Return lists of property meta objects and their names for properties that have no column name. =head1 METHODS =item property_meta_for_name $property_obj = $class_obj->property_meta_for_name($property_name); Return the L object in the class's inheritance hierarchy with the given name. If the property name has been overridden somewhere in the hierarchy, then it will return the property object most specific to the class. =item id_property_sorter $subref = $class_obj->id_property_sorter; @sorted_objs = sort $subref @unsorted_objs; Returns a subroutine reference that can be used to sort object instances of the class. The subref is able to handle classes with multiple ID properties, and mixes of numeric and non-numeric data and data types. =item autogenerate_new_object_id This method is called whenever new objects of the given class are created through Ccreate()>, and not all of their ID properties were specified. UR::Object::Type has an implementation used by default, but other classes can override this if they need special handling. =back =head1 SEE ALSO L =cut Tag.pm000444023532023421 765212121654173 15205 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Tag; #TODO: update these to be UR::Value objects instead of some ancient hack =pod =head1 NAME UR::Object::Tag - Transitory attribute tags for a UR::Object at a given time. =head1 SYNOPSIS if (my @attribs = grep { $_->type eq 'invalid' } $obj->attribs()) { print $obj->display_name . " has invalid attributes. They are:\n"; for my $atrib (@attribs) { print join(",",$attrib->properties) . ":" . $attrib->desc . "\n"; } } Project H_NHF00 has invalid attributes, they are: project_subdirectory : Directory does not exist. target, status : Target cannot be null for projects with an active status. =head1 DESCRIPTION Objects of this class are created by create_attribs() on classes derived from UR::Object. They are retrieved by UR::Object->attribs(). =head1 INHERITANCE This class inherits from UR::ModuleBase. =head1 OBJECT METHODS =over 4 =item type A single-word description of the attribute which categorizes the attribute. Common attribute types are: =over 6 =item invalid Set when the object has invalid properties and cannot be saved. =item changed Set when the object is different than its "saved" version. =item hidden Set when the object has properties which should not be shown. =item editable Set when some part of the object is editable in the current context. =item warning Set when a warning about the state of the object is in effect. =item match Set when a search which is in effect matches this object's property(s). =item comment Set when this attribute is just an informational message. =back =item properties A list of properties to which the attribute applies. This is null when the attribute applies to the whole object, but typically returns one property name. Occasionally, it returns more than one property. Very rarely (currently never), the property may be in the form of an arrayref like: [ class_name, id, property_name ], in which case the property may actually be that of another related object. =item desc A string of text giving detail to the attribute. =back =head1 CLASS METHODS =over 4 =item create Makes a new UR::Object::Tag. =item delete Throws one away. =item filter Sets/gets a filter to be applied to all attribute lists returned in the application. This gives the application developer final veto power over expressed attributes in the app. In most cases, developers will write view components which use attributes, and will ignore them rather than plug-in at this low level to augment/mangle/supress. The filter will be given an object reference and a refrence to an array of attributes which are tentatively to be delivered for the object. =cut # set up package require 5.006_000; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION; # set up module use base qw(UR::ModuleBase); our (@EXPORT, @EXPORT_OK); @EXPORT = qw(); @EXPORT_OK = qw(); ##- use UR::Util; our %default_values = ( type => undef, properties => [], desc => undef ); UR::Util->generate_readwrite_methods(%default_values); *type_name = \&type; *property_names = \&properties; *description = \&description; sub create($@) { my ($class, @initial_prop) = @_; my $self = bless({%default_values,@initial_prop},$class); if (not ref($self->{properties}) eq 'ARRAY') { $self->{properties} = [ $self->{properties} ]; } return $self; } sub delete($) { UR::DeletedRef->bury($_[0]) } our $filter; sub filter { if (@_ > 1) { my $old = $filter; $filter = $_[1]; return $old; } return $filter; } sub __display_name__ { my $self = shift; my $desc = $self->desc; my $prefix = uc($self->type); my @properties = map { "'$_'" } $self->properties; my $prop_noun = scalar(@properties) > 1 ? 'properties' : 'property'; my $msg = "$prefix: $prop_noun " . join(', ', @properties) . ": $desc"; return $msg; } 1; __END__ =pod =back =head1 SEE ALSO UR::Object(3) =cut #$Header$ Iterator.pm000444023532023421 603512121654173 16255 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Iterator; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; our @CARP_NOT = qw( UR::Object ); # These are no longer UR Objects. They're regular blessed references that # get garbage collected in the regular ways #use UR; # #UR::Object::Type->define( # class_name => __PACKAGE__, # has => [ # filter_rule_id => {}, # ], #); # #sub create_for_filter_rule { # my $class = shift; # my $filter_rule = shift; # my $code = $UR::Context::current->get_objects_for_class_and_rule($filter_rule->subject_class_name,$filter_rule,undef,1); # # my $self = $class->SUPER::create( # # TODO: some bug with frozen items? # filter_rule_id => $filter_rule->id, # ); # # $self->_iteration_closure($code); # return $self; #} sub create { die "Don't call UR::Object::Iterator->create(), use create_for_filter_rule() instead"; } sub create_for_filter_rule { my $class = shift; my $filter_rule = shift; my $code = $UR::Context::current->get_objects_for_class_and_rule($filter_rule->subject_class_name,$filter_rule,undef,1); my $self = bless { filter_rule_id => $filter_rule->id, _iteration_closure => $code}, __PACKAGE__; return $self; } sub _iteration_closure { my $self = shift; if (@_) { return $self->{_iteration_closure} = shift; } $self->{_iteration_closure}; } sub next { shift->{_iteration_closure}->(@_); } 1; =pod =head1 NAME UR::Object::Iterator - API for iterating through objects matching a rule =head1 SYNOPSIS my $rule = UR::BoolExpr->resolve('Some::Class', foo => 1); my $iter = UR::Object::Iterator->create_for_filter_rule($rule); while (my $obj = $iter->next()) { print "Got an object: ",$obj->id,"\n"; } # Equivalent my $iter2 = Some::Class->create_iterator(foo => 1); while (my $obj = $iter2->next()) { print "Got an object: ",$obj->id,"\n"; } =head1 DESCRIPTION get(), implemented in UR::Object, is the usual way for retrieving sets of objects matching particular properties. When the result set of data is large, it is often more efficient to use an iterator to access the data instead of getting it all in one list. UR::Object implements create_iterator(), which is just a wrapper around create_for_filter_rule(). UR::Object::Iterator instances are normal Perl object references, not UR-based objects. They do not live in the Context's object cache, and obey the normal Perl rules about scoping. =head1 METHODS =over 4 =item create_for_filter_rule $iter = UR::Object::Iterator->create_for_filter_rule($boolexpr); Creates an iterator object based on the given BoolExpr (rule). Under the hood, it calls get_objects_for_class_and_rule() on the current Context with the $return_closure flag set to true. =item next $obj = $iter->next(); Return the next object matching the iterator's rule. When there are no more matching objects, it returns undef. =back =head1 SEE ALSO UR::Object, UR::Context =cut Ghost.pm000444023532023421 577112121654173 15556 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object##### # # Support "Ghost" objects. These represent deleted items which are not saved. # They are omitted from regular class lists. # ##### package UR::Object::Ghost; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub _init_subclass { my $class_name = pop; no strict; no warnings; my $live_class_name = $class_name; $live_class_name =~ s/::Ghost$//; *{$class_name ."\:\:class"} = sub { "$class_name" }; *{$class_name ."\:\:live_class"} = sub { "$live_class_name" }; } sub create { Carp::croak('Cannot create() ghosts.') }; sub delete { Carp::croak('Cannot delete() ghosts.') }; sub _load { shift->is_loaded(@_); } sub unload { return; } sub __errors__ { return; # Ghosts are always valid, don't check their properties } sub edit_class { undef } sub ghost_class { undef } sub is_ghost { return 1; } sub live_class { my $class = $_[0]->class; $class =~ s/::Ghost//; return $class; } my @ghost_changes; sub changed { @ghost_changes = UR::Object::Tag->create ( type => 'changed', properties => ['id']) unless @ghost_changes; return @ghost_changes; } sub AUTOSUB { # Delegate to the similar function on the regular class. my ($func, $self) = @_; my $live_class = $self->live_class; return $live_class->can($func); } 1; =pod =head1 NAME UR::Object::Ghost - Abstract class for representing deleted objects not yet committed =head1 SYNOPSIS my $obj = Some::Class->get(1234); $obj->some_method(); $obj->delete(); # $obj is now a UR::DeletedRef $ghost = Some::Class::Ghost->get(1234); $ghost->some_method; # Works =head1 DESCRIPTION Ghost objects are a bookkeeping entity for tracking objects which have been loaded from an external data source, deleted within the application, and not yet committed. This implies that they still exist in the external data source. When the Context is committed, the existence of Ghost objects triggers commands to the external data sources to also delete the object(s). When objects are brought into the Context by querying a data source, they are compared against any ghosts that may already exist, and matching objects are not re-loaded or returned to the user from a call to get(). If the user wants to get Ghost objects, they must call get() explicitly on the Ghost class. Each class in the system also has an associated Ghost class, the name of which is formed by tacking '::Ghost' to the name of the regular class. Ghost classes do not have ghosts themselves. Instances of Ghosts are not instantiated with create() directly, they are created as a concequence of deleting a regular object instance. A Ghost can be turned back into a "live" object by re-creating it, or rolling back the transaction it was deleted in. =head1 DEPRECATED Applications will not, and should not, normally interact with Ghosts. The whole Ghost system is scheduled for elimination as we refactor the Context and software transaction framework. =head1 SEE ALSO UR::Object, UR::Object::Type =cut View.pm000444023532023421 5017012121654173 15415 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::View; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; class UR::Object::View { has_abstract_constant => [ subject_class_name => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, perspective => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, toolkit => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 }, ], has_optional => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => 'when nested inside another view, this references that view', }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being observed' }, aspects => { is => 'UR::Object::View::Aspect', reverse_as => 'parent_view', is_many => 1, specify_by => 'name', order_by => 'number', doc => 'the aspects of the subject this view renders' }, default_aspects => { is => 'ARRAY', is_abstract => 1, is_constant => 1, is_many => 1, # technically this is one "ARRAY" default_value => undef, doc => 'a tree of default aspect descriptions' }, ], has_optional_transient => [ _widget => { doc => 'the object native to the specified toolkit which does the actual visualization' }, _observer_data => { is => 'HASH', is_transient => 1, value => undef, # hashref set at construction time doc => ' hooks around the subject which monitor it for changes' }, ], has_many_optional => [ aspect_names => { via => 'aspects', to => 'name' }, ] }; sub create { my $class = shift; my ($params,@extra) = $class->define_boolexpr(@_); # set values not specified in the params which can be inferred from the class name my ($expected_class,$expected_perspective,$expected_toolkit) = ($class =~ /^(.*)::View::(.*?)::([^\:]+)$/); unless ($params->specifies_value_for('subject_class_name')) { $params = $params->add_filter(subject_class_name => $expected_class); } unless ($params->specifies_value_for('perspective')) { $expected_perspective = join('-', split( /(?=[A-Z])/, $expected_perspective) ); #convert CamelCase to hyphenated-words $params = $params->add_filter(perspective => $expected_perspective); } unless ($params->specifies_value_for('toolkit')) { $params = $params->add_filter(toolkit => $expected_toolkit); } # now go the other way, and use both to infer a final class name $expected_class = $class->_resolve_view_class_for_params($params); unless ($expected_class) { my $subject_class_name = $params->value_for('subject_class_name'); Carp::croak("Failed to resolve a subclass of " . __PACKAGE__ . " for $subject_class_name from parameters. " . "Received $params."); } unless ($class->isa($expected_class)) { return $expected_class->create(@_); } $params->add_filter(_observer_data => {}); my $self = $expected_class->SUPER::create($params); return unless $self; $class = ref($self); $expected_class = $class->_resolve_view_class_for_params( subject_class_name => $self->subject_class_name, perspective => $self->perspective, toolkit => $self->toolkit ); unless ($expected_class and $expected_class eq $class) { $expected_class ||= ''; Carp::croak("constructed a $class object but properties indicate $expected_class should have been created."); } unless ($params->specifies_value_for('aspects')) { my @aspect_specs = $self->default_aspects(); if (! @aspect_specs) { @aspect_specs = $self->_resolve_default_aspects(); } if (@aspect_specs == 1 and ref($aspect_specs[0]) eq 'ARRAY') { # Got an arrayref, expand back into an array @aspect_specs = @{$aspect_specs[0]}; } for my $aspect_spec (@aspect_specs) { my $aspect = $self->add_aspect(ref($aspect_spec) ? %$aspect_spec : $aspect_spec); unless ($aspect) { $self->error_message("Failed to add aspect @$aspect_spec to new view " . $self->id); $self->delete; return; } } } return $self; } our %view_class_cache = (); sub _resolve_view_class_for_params { # View modules use standardized naming: SubjectClassName::View::Perspective::Toolkit. # The subject must be explicitly of class "SubjectClassName" or some subclass of it. my $class = shift; my $bx = $class->define_boolexpr(@_); if (exists $view_class_cache{$bx->id}) { if (!defined $view_class_cache{$bx->id}) { return; } return $view_class_cache{$bx->id}; } my %params = $bx->params_list; my $subject_class_name = delete $params{subject_class_name}; my $perspective = delete $params{perspective}; my $toolkit = delete $params{toolkit}; my $aspects = delete $params{aspects}; unless($subject_class_name and $perspective and $toolkit) { Carp::confess("Bad params @_. Expected subject_class_name, perspective, toolkit."); } $perspective = lc($perspective); $toolkit = lc($toolkit); my $namespace = $subject_class_name->__meta__->namespace; my $vocabulary = ($namespace and $namespace->can("get_vocabulary") ? $namespace->get_vocabulary() : undef); $vocabulary = UR->get_vocabulary; my $subject_class_object = $subject_class_name->__meta__; my @possible_subject_class_names = ($subject_class_name,$subject_class_name->inheritance); my $subclass_name; for my $possible_subject_class_name (@possible_subject_class_names) { $subclass_name = join("::", $possible_subject_class_name, "View", join ("", $vocabulary->convert_to_title_case ( map { ucfirst(lc($_)) } split(/-+|\s+/,$perspective) ) ), join ("", $vocabulary->convert_to_title_case ( map { ucfirst(lc($_)) } split(/-+|\s+/,$toolkit) ) ) ); my $subclass_meta; eval { $subclass_meta = $subclass_name->__meta__; }; if ($@ or not $subclass_meta) { #not a class... keep looking next; } unless($subclass_name->isa(__PACKAGE__)) { Carp::carp("Subclass $subclass_name exists but is not a view?!"); next; } $view_class_cache{$bx->id} = $subclass_name; return $subclass_name; } $view_class_cache{$bx->id} = undef; return; } sub _resolve_default_aspects { my $self = shift; my $parent_view = $self->parent_view; my $subject_class_name = $self->subject_class_name; my $meta = $subject_class_name->__meta__; my @c = ($meta->class_name, $meta->ancestry_class_names); my %aspects = map { $_->property_name => 1 } grep { not $_->implied_by } UR::Object::Property->get(class_name => \@c); my @aspects = sort keys %aspects; return @aspects; } sub __signal_change__ { # ensure that changes to the view which occur # after the widget is produced # are reflected in the widget my ($self,$method,@details) = @_; if ($self->_widget) { if ($method eq 'subject' or $method =~ 'aspects') { $self->_bind_subject(); } elsif ($method eq 'delete' or $method eq 'unload') { my $observer_data = $self->_observer_data; for my $subscription (values %$observer_data) { my ($class, $id, $method, $callback) = @$subscription; $class->cancel_change_subscription($id, $method, $callback); } $self->_widget(undef); } } return 1; } # _encompassing_view() and _subject_is_used_in_an_encompassing_view() are used by the # default views (UR::Object::View::Default::*) to detect an infinite recursion situation # where it's asked to render an object A that references a B which refers back to A # If this view is embedded in another view, return the encompassing view sub _encompassing_view { my $self = shift; my @aspects = UR::Object::View::Aspect->get(delegate_view_id => $self->id); if (@aspects) { # FIXME - is it possible for there to be more than one thing in @aspects here? # And if so, how do we differentiate them return $aspects[0]->parent_view; } # $self must be the top-level view return; } # If the subject of the view is also the subject of an encompassing view, return true sub _subject_is_used_in_an_encompassing_view { my($self,$subject) = @_; $subject = $self->subject unless (@_ == 2); my $encompassing = $self->_encompassing_view; while($encompassing) { if ($encompassing->subject eq $subject) { return 1; } else { $encompassing = $encompassing->_encompassing_view(); } } return; } sub all_subject_classes { my $self = shift; my @classes = (); # suppress error callbacks inside this method my $old_cb = UR::ModuleBase->message_callback('error'); UR::ModuleBase->message_callback('error', sub {}) if ($old_cb); for my $aspect ($self->aspects) { unless ($aspect->delegate_view) { eval { $aspect->generate_delegate_view; }; } if ($aspect->delegate_view) { push @classes, $aspect->delegate_view->all_subject_classes } } UR::ModuleBase->message_callback('error', $old_cb) if ($old_cb); push @classes, $self->subject_class_name; my %saw; my @uclasses = grep(!$saw{$_}++,@classes); return @uclasses; } sub all_subject_classes_ancestry { my $self = shift; my @classes = $self->all_subject_classes; my @aclasses; for my $class (@classes) { my $m; eval { $m = $class->__meta__ }; next if $@ or not $m; push @aclasses, reverse($class, $m->ancestry_class_names); } my %saw; my @uaclasses = grep(!$saw{$_}++,@aclasses); return @uaclasses; } # rendering implementation sub widget { my $self = shift; if (@_) { Carp::confess("Widget() is not settable! Its value is set from _create_widget() upon first use."); } my $widget = $self->_widget(); unless ($widget) { $widget = $self->_create_widget(); return unless $widget; $self->_widget($widget); $self->_bind_subject(); # works even if subject is undef } return $widget; } sub _create_widget { Carp::confess("The _create_widget method must be implemented for all concrete " . " view subclasses. No _create_widget for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } sub _bind_subject { # This is called whenever the subject changes, or when the widget is first created. # It handles the case in which the subject is undef. my $self = shift; my $subject = $self->subject(); return unless defined $subject; my $observer_data = $self->_observer_data; unless ($observer_data) { $self->_observer_data({}); $observer_data = $self->_observer_data; } Carp::confess unless $self->_observer_data == $observer_data; # See if we've already done this. return 1 if $observer_data->{$subject}; # Wipe subscriptions from the last bound subscription(s). for (keys %$observer_data) { my $s = delete $observer_data->{$_}; my ($class, $id, $method,$callback) = @$s; $class->cancel_change_subscription($id, $method,$callback); } return unless $subject; # Make a new subscription for this subject my $subscription = $subject->create_subscription( callback => sub { $self->_update_view_from_subject(@_); } ); $observer_data->{$subject} = $subscription; # Set the view to show initial data. $self->_update_view_from_subject; return 1; } sub _update_view_from_subject { # This is called whenever the view changes, or the subject changes. # It passes the change(s) along, so that the update can be targeted, if the developer chooses. Carp::croak("The _update_view_from_subject method must be implemented for all concreate " . " view subclasses. No _update_subject_from_view for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } sub _update_subject_from_view { Carp::croak("The _update_subject_from_view method must be implemented for all concreate " . " view subclasses. No _update_subject_from_view for " . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!"); } # external controls sub show { my $self = shift; $self->_toolkit_package->show_view($self); } sub show_modal { my $self = shift; $self->_toolkit_package->show_view_modally($self); } sub hide { my $self = shift; $self->_toolkit_package->hide_view($self); } sub _toolkit_package { my $self = shift; my $toolkit = $self->toolkit; return "UR::Object::View::Toolkit::" . ucfirst(lc($toolkit)); } 1; =pod =head1 NAME UR::Object::View - a base class for "views" of UR::Objects =head1 SYNOPSIS $object = Acme::Product->get(1234); ## Acme::Product::View::InventoryHistory::Gtk2 $view = $object->create_view( perspective => 'inventory history', toolkit => 'gtk2', ); $widget = $view->widget(); # returns the Gtk2::Widget itself directly $view->show(); # puts the widget in a Gtk2::Window and shows everything ## $view = $object->create_view( perspective => 'inventory history', toolkit => 'xml', ); $widget = $view->widget(); # returns an arrayref with the xml string reference, and the output filehandle (stdout) $view->show(); # prints the current xml content to the handle $xml = $view->content(); # returns the XML directly ## $view = $object->create_view( perspective => 'inventory history', toolkit => 'html', ); $widget = $view->widget(); # returns an arrayref with the html string reference, and the output filehandle (stdout) $view->show(); # prints the html content to the handle $html = $view->content(); # returns the HTML text directly =head1 USAGE API =over 4 =item create The constructor requires that the subject_class_name, perspective, and toolkit be set. Most concrete subclasses have perspective and toolkit set as constant. Producing a view object does not "render" the view, just creates an interface for controlling the view, including encapsualting its creation. The subject can be set later and changed. The aspects viewed may be constant for a given perspective, or mutable, depending on how flexible the of the perspective logic is. =item show For stand-alone views, this puts the view widget in its a window. For views which are part of a larger view, this makes the view widget visible in the parent. =item hide Makes the view invisible. This means hiding the window, or hiding the view widget in the parent widget for subordinate views. =item show_modal This method shows the view in a window, and only returns after the window is closed. It should only be used for views which are a full interface capable of closing itself when done. =item widget Returns the "widget" which renders the view. This is built lazily on demand. The actual object type depends on the toolkit named above. This method might return HTML text, or a Gtk object. This can be used directly, and is used internally by show/show_modal. (Note: see UR::Object::View::Toolkit::Text for details on the "text" widget, used by HTML/XML views, etc. This is just the content and an I/O handle to which it should stream.) =item delete Delete the view (along with the widget(s) and infrastructure underlying it). =back =head1 CONSTRUCTION PROPERTIES (CONSTANT) The following three properties are constant for a given view class. They determine which class of view to construct, and must be provided to create(). =over 4 =item subject_class_name The class of subject this view will view. Constant for any given view, but this may be any abstract class up-to UR::Object itself. =item perspective Used to describe the layout logic which gives logical content to the view. =item toolkit The specific (typically graphical) toolkit used to construct the UI. Examples are Gtk, Gkt2, Tk, HTML, XML. =back =head1 CONFIGURABLE PROPERTIES These methods control which object is being viewed, and what properties of the object are viewed. They can be provided at construction time, or afterward. =over 4 =item subject The particular "model" object, in MVC parlance, which is viewed by this view. This value may change =item aspects / add_aspect / remove_aspect Specifications for properties/methods of the subject which are rendered in the view. Some views have mutable aspects, while others merely report which aspects are revealed by the perspective in question. An "aspect" is some characteristic of the "subject" which is rendered in the view. Any property of the subject is usable, as is any method. =back =head1 IMPLEMENTATION INTERFACE When writing new view logic, the class name is expected to follow a formula: Acme::Rocket::View::FlightPath::Gtk2 \ / \ / \ subject class name perspective toolkit The toolkit is expected to be a single word. The perspective is everything before the toolkit, and after the last 'View' word. The subject_class_name is everything to the left of the final '::View::'. There are three methods which require an implementation, unless the developer inherits from a subclass of UR::Object::View which provides these methods: =over 4 =item _create_widget This creates the widget the first time ->widget() is called on a view. This should be implemented in a given perspective/toolkit module to actually create the GUI using the appropriate toolkit. It will be called before the specific subject is known, so all widget creation which is subject-specific should be done in _bind_subject(). As such it typically only configures skeletal aspects of the view. =item _bind_subject This method is called when the subject is set, or when it is changed, or unset. It updates the widget to reflect changes to the widget due to a change in subject. This method has a default implementation which does a general subscription to changes on the subject. It probably does not need to be overridden in custom views. Implementations which _do_ override this should take an undef subject, and be sure to un-bind a previously existing subject if there is one set. =item _update_view_from_subject If and when the property values of the subject change, this method will be called on all views which render the changed aspect of the subject. =item _update_subject_from_view When the widget changes, it should call this method to save the UI changes to the subject. This is not applicable to read-only views. =back =head1 OTHER METHODS =over 4 =item _toolkit_package This method is useful to provide generic toolkit-based services to a view, using a toolkit agnostic API. It can be used in abstract classes which, for instance, want to share logic for a given perspective across toolkits. The toolkit class related to a view is responsible for handling show/hide logic, etc. in the base UR::Object::View class. Returns the name of a class which is derived from UR::Object::View::Toolkit which implements certain utility methods for views of a given toolkit. =back =head1 EXAMPLES $o = Acme::Product->get(1234); $v = Acme::Product::View::InventoryHistory::HTML->create(); $v->add_aspect('outstanding_orders'); $v->show; =cut Type.pm000444023532023421 4472612121654174 15437 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Type; use warnings; use strict; require UR; # Used during bootstrapping. our @ISA = qw(UR::Object); our $VERSION = "0.41"; # UR $VERSION;; our @CARP_NOT = qw( UR::Object UR::Context UR::ModuleLoader Class::Autouse UR::BoolExpr ); # Most of the API for this module are legacy internals required by UR. use UR::Object::Type::InternalAPI; # This module implements define(), and most everything behind it. use UR::Object::Type::Initializer; # The methods used by the initializer to write accessors in perl. use UR::Object::Type::AccessorWriter; # The methods to extract/(re)create definition text in the module source file. use UR::Object::Type::ModuleWriter; # Present the internal definer as an external method sub define { shift->__define__(@_) } # For efficiency, certain hash keys inside the class cache property metadata # These go in this array, and are cleared when property metadata is mutated our @cache_keys; # This is the function behind $class_meta->properties(...) # It mimics the has-many object accessor, but handles inheritance # Once we have "isa" and "is-parent-of" operator we can do this with regular operators. push @cache_keys, '_properties'; sub _properties { my $self = shift; my $all = $self->{_properties} ||= do { # start with everything, as it's a small list my $map = $self->_property_name_class_map; my @all; for my $property_name (sort keys %$map) { my $class_names = $map->{$property_name}; my $class_name = $class_names->[0]; my $id = $class_name . "\t" . $property_name; my $property_meta = UR::Object::Property->get($id); unless ($property_meta) { Carp::confess("Failed to find property meta for $class_name $property_name?"); } push @all, $property_meta; } \@all; }; if (@_) { my ($bx, %extra) = UR::Object::Property->define_boolexpr(@_); my @matches = grep { $bx->evaluate($_) } @$all; if (%extra) { # Additional meta-properties on meta-properties are not queryable until we # put the UR::Object::Property into a private sub-class. # This will give us most of the functionality. for my $key (keys %extra) { my ($name,$op) = ($key =~ /(\w+)\s*(.*)/); my @have_the_property = grep { $_->can($name) } @$all; if (@have_the_property == 0) { die "unknown property $name used to query properties of " . $self->class_name; } if ($op and $op ne '==' and $op ne 'eq') { die "operations besides equals are not supported currently for added meta-properties like $name on class " . $self->class_name; } my $value = $extra{$key}; no warnings; @matches = grep { $_->can($name) and $_->$name eq $value } @matches; } } return if not defined wantarray; return @matches if wantarray; die "Matched multiple meta-properties, but called in scalar context!" . Data::Dumper::Dumper(\@matches) if @matches > 1; return $matches[0]; } else { @$all; } } sub property { if (@_ == 2) { # optimize for the common case my ($self, $property_name) = @_; my $class_names = $self->_property_name_class_map->{$property_name}; return unless $class_names and @$class_names; my $id = $class_names->[0] . "\t" . $property_name; return UR::Object::Property->get($id); } else { # this forces scalar context, raising an exception if # the params used result in more than one match my $one = shift->properties(@_); return $one; } } push @cache_keys, '_property_names'; sub property_names { my $self = $_[0]; my $names = $self->{_property_names} ||= do { my @names = sort keys %{ shift->_property_name_class_map }; \@names; }; return @$names; } push @cache_keys, '_property_name_class_map'; sub _property_name_class_map { my $self = shift; my $map = $self->{_property_name_class_map} ||= do { my %map = (); for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if (my $has = $class_meta->{has}) { for my $key (sort keys %$has) { my $classes = $map{$key} ||= []; push @$classes, $class_name; } } } \%map; }; return $map; } # The prior implementation of _properties() (behind ->properties()) # filtered out certain property meta. This is the old version. # The new version above will return one object per property name in # the meta ancestry. sub _legacy_properties { my $self = shift; if (@_) { my $bx = UR::Object::Property->define_boolexpr(@_); my @matches = grep { $bx->evaluate($_) } $self->property_metas; return if not defined wantarray; return @matches if wantarray; die "Matched multiple meta-properties, but called in scalar context!" . Data::Dumper::Dumper(\@matches) if @matches > 1; return $matches[0]; } else { $self->property_metas; } } 1; =pod =head1 NAME UR::Object::Type - a meta-class for any class or primitive type =head1 SYNOPSIS use UR; class MyClass { is => ['ParentClass1', 'ParentClass2'], id_by => [ id_prop1 => { is => 'Integer' }, id_prop2 => { is => 'String' }, ], has => [ property_a => { is => 'String' } property_b => { is => 'Integer', is_optional => 1 }, ], }; my $meta = MyClass->__meta__; my @parent_class_metas = $meta->parents(); # 2 meta objects, see UR::Object::Property my @property_meta = $meta->properties(); # N properties (4, +1 from UR::Object, +? from ParentClass1 and ParentClass2) $meta->is_abstract; $meta->... =head1 DESCRIPTION UR::Object::Type implements the class behind the central metadata in the UR class framework. It contains methods for introspection and manipulation of related class data. A UR::Object::Type object describes UR::Object, and also every subclass of UR::Object. =head1 INHERITANCE Each sub-class of UR::Object has a single UR::Object::Type object describing the class. The UR::Object class itself also has a UR::Object::Type object describing the base class of the system. In addition to describing UR::Object an each of its subclasses, UR::Object::Type is _itself_ a subclass of L. This means that the same query APIs used for regular objects can be used for meta objects. /-----------------------\ V | UR::Object -> has-meta -> UR::Object::Type --> has-meta >--/ A | \ / \-----<- is-a <-------/ Further, new classes which generate a new UR::Object::Type, also generate a private subclass for the meta-class. This means that each new class can have private meta methods, (ala Ruby). This also means that extensions to a meta-class, apply to the meta-class of its derivatives. Regular Meta-Class Entity Singleton ------- ---------- Greyhound has-meta -> Greyhound::Type | | V V is-a is-a | | V V Dog has-meta -> Dog::Type | | V V is-a is-a | | V V Animal has-meta -> Animal::Type | | V V is-a is-a | | /-----------------\ V V V | UR::Object has-meta -> UR::Object::Type has-meta -/ A is-a | | \________________________/ =head1 CONSTRUCTORS =over 4 =item "class" class MyClass1 {}; class MyClass2 { is => 'MyClass1' }; class MyClass3 { is => ['Parent1','Parent2'], is_abstract => 1, is_transient => 1, has => [ qw/p1 p2 p3/ ], doc => 'woo hoo!' }; The primary constructor is not a method on this class at all. UR catches "class SOMENAME { ... }" and calls define() with the parameters. =item define my $class_obj = UR::Object::Type->define( class_name => 'MyClass', ... ); Register a class with the system. The given class_name must be unique within the application. As a side effect, a new Perl namespace will be created for the class's name, and methods will be injected into that namespace for any of the class properties. Other types of metadata objects will get created to manage the properties and relationships to other classes. See the L documentation for more information about the parameters C accepts. =item create my $class_obj = UR::Object::Type->create( class_name => 'Namespace::MyClass', ... ); Create a brand new class within an already existing UR namespace. C takes all the same parameters as C. Another side effect of create is that when the application commits its Context, a new Perl module will be created to implement the class, complete with a class definition. Applications will not normally use create(). =back =head1 PROPERTIES Each property has a method of the same name =head2 External API =over 4 =item class_name $name = $class_obj->class_name The full name of the class. This is symmetrical with $class_obj = $name->__meta__. =item properties @all = $class_obj->properties(); @some = $class_obj->properties( 'is => ['Text','Number'] 'doc like' => '%important%', 'property_name like' => 'someprefix_%', ); Access the related property meta-objects for all properties of this class. It includes the properties of any parent classes which are inherited by this class. See L for details. =item id_properties @all_id = $class_obj->id_properties(); @some = $class_obj->properties( 'is => ['Text','Number'] 'doc like' => '%important%', 'property_name like' => 'someprefix_%', ); Like properties(), but only returns ID property metadata. =item property $property_meta = $class_obj->property('someproperty'); The singular version of the above. A single argument, as usual, is treated as the remainder of the ID, and will select a property by name. =item property_names @names = $class_obj->property_names; Returns a list of all properties belonging to the class, directly or through inheritance. =item namespace $namespace_name = $class_obj->namespace Returns the name of the class's UR namespace. =item doc $doc = $class_obj->doc A place to put general class-specific notes. =item data_source_id $ds_id = $class_obj->data_source_id The name of the external data source behind this class. Classes without data sources cannot be saved and exist only during the life of the application. data_source_id will resolve to an L id. =item table_name $table_name = $class_object->table_name For classes with data sources, this is the name of the table within that data source. This is usually a table in a relational database. At a basic level, it is a storage directive interpreted by the data_source, and may or may not related to a storage table at that level. =item is_abstract $bool = $class_obj->is_abstract A flag indicating if this is an abstract class. Abstract classes cannot have instances, but can be inherited by other classes. =item is_final $bool = $class_obj->is_final A flag indicating if this class cannot have subclasses. =item is_singleton $bool = $class_obj->is_singleton A flag indicating whether this is a singleton class. If true, the class will inherit from L. =item is_transactional $bool = $class_obj->is_transactional A flag indicating whether changes to this class's instances will be tracked. Non-transactional objecs do not change when an in-memory transaction rolls back. It is similar to the is_transient meta-property, which does the same for an individual property. =back =head2 Internal API These methods return data about how this class relates to other classes. =over 4 =item namespace_meta $ns_meta = $class_obj->namespace_meta Returns the L object with the class's namespace name. =item parent_class_names @names = $class_obj->parent_class_names Returns a list of the immediate parent classes. =item parent_class_metas @class_objs = $class_obj->parent_class_metas Returns a list of the class objects (L instances) of the immediate parent classes =item ancestry_class_names @names = $class_obj->ancestry_class_names Returns a list of all the class names this class inherits from, directly or indirectly. This list may have duplicate names if there is multiple inheritance in the family tree. =item ancestry_class_metas @class_objs = $class_obj->ancestry_class_metas Returns a list of the class objects for each inherited class. =item direct_property_names @names = $class_obj->direct_property_names Returns a list of the property names defined within this class. This list will not include the names of any properties inherited from parent classes unless they have been overridden. =item direct_property_metas @property_objs = $class_obj->direct_property_metas Returns a list of the L objects for each direct property name. =item ancestry_property_names @names = $class_obj->ancestry_property_names Returns a list of property names of the parent classes and their inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item ancestry_property_metas @property_objs = $class_obj->ancestry_property_metas; Returns a list of the L objects for each ancestry property name. =item all_property_names Returns a list of property names of the given class and its inheritance heirarchy. The list may include duplicates if a property is overridden somewhere in the heirarchy. =item all_property_metas @property_objs = $class_obj->all_property_metas; Returns a list of the L objects for each name returned by all_property_names. =item direct_id_property_names @names = $class_obj->direct_id_property_names Returns a list of the property names designated as "id" properties in the class definition. =item direct_id_property_metas @property_objs = $class_obj->direct_id_property_metas Returns a list of the L objects for each id property name. =item ancestry_id_property_names =item ancestry_id_property_metas =item all_id_property_names =item all_id_property_metas @names = $class_obj->ancestry_id_property_names; @property_objs = $class_obj->ancestry_id_property_metas; @names = $class_obj->all_id_property_names; @property_objs = $class_obj->all_id_property_metas; Returns the property names or L objects for either the parent classes and their inheritance heirarchy, or for the given class and all of its inheritance heirarchy. The lists may include duplicates if properties are overridden somewhere in the heirarchy. =item unique_property_set_hashref $constraints = $class_obj->unique_property_set_hashref Return a hashref describing the unique constraints on the given class. The keys of $constraint are constraint names, and the values are listrefs of property names that make up the unique constraint. =item add_unique_constraint $class_obj->add_unique_constraint($constraint_name, @property_name_list) Add a unique constraint to the given class. It is an exception if the given $constraint_name already exists as a constraint on this class or its parent classes. =item remove_unique_constraint $class_obj->remove_unique_constraint($constraint_name) Remove a unique constraint from the given class. It is an exception if the given constraint name does not exist. =item ancestry_table_names =item all_table_names @names = $class_obj->ancestry_table_names Returns a list of table names in the class's inheritance heirarchy. =item direct_column_names Returns a list of column names for each direct property meta. Classes with data sources and table names will have properties with column names. =item direct_id_column_names Returns a list of ID column names for each direct property meta. =item direct_columnless_property_names =item direct_columnless_property_metas =item ancestry_columnless_property_names =item ancestry_columnless_property_metas =item all_columnless_property_names =item all_columnless_property_metas Return lists of property meta objects and their names for properties that have no column name. =back =head1 METHODS =over =item property_meta_for_name $property_obj = $class_obj->property_meta_for_name($property_name); Return the L object in the class's inheritance hierarchy with the given name. If the property name has been overridden somewhere in the hierarchy, then it will return the property object most specific to the class. =item id_property_sorter $subref = $class_obj->id_property_sorter; @sorted_objs = sort $subref @unsorted_objs; Returns a subroutine reference that can be used to sort object instances of the class. The subref is able to handle classes with multiple ID properties, and mixes of numeric and non-numeric data and data types. =item autogenerate_new_object_id This method is called whenever new objects of the given class are created through Ccreate()>, and not all of their ID properties were specified. UR::Object::Type has an implementation used by default, but other classes can override this if they need special handling. =back =head1 SEE ALSO L =cut Accessorized.pm000444023532023421 1243612121654175 17126 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Accessorized; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::Object'], ); #--- just because I'm tired of GSCApp and Class::Accessor not playing nice, here we go sub delegate{ my $class = shift; my %p = @_; foreach my $get_object_method(keys %p){ foreach my $delegated_function(@{$p{$get_object_method}}){ my $class_function = $class.'::'.$delegated_function; no strict; *$class_function = sub{ my $self = shift; my $obj = $self->$get_object_method; #--- get the object of delgation unless($obj){ $self->error_message("Failed to call $function on $self"); return; } return $obj->$delegated_function(@_); }; } } 1; } sub ro_delegate{ my $class = shift; my %p = @_; foreach my $function (keys %p){ foreach my $delegator_func(@_){ my $class_function = $class.'::'.$delegator_func; no strict; *$class_function = sub{ my $self = shift; my $obj = $self->$function(); #--- get the object of delgation unless($obj){ $self->error_message("Failed to call $function on $self"); return; } return $obj->$delegator_func(); }; } } 1; } sub accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $setfunc = $class.'::'.$accessor_func; no strict; *$setfunc = sub{ my $self = shift; if(@_){ return $self->__set($accessor_func, @_); } return $self->__get($accessor_func); }; } } sub explicit_accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $write_func = $class.'::set_'.$accessor_func; my $read_func = $class.'::get_'.$accessor_func; no strict; *$write_func = sub{ my $self = shift; return unless @_; return $self->__set($accessor_func, @_); }; *$read_func = sub{ my $self = shift; return unless @_; return $self->__get($accessor_func, @_); }; } } sub ro_accessorize{ my $class = shift; foreach my $accessor_func(@_){ my $setfunc = $class.'::get_'.$accessor_func; no strict; *$setfunc = sub{ my $self = shift; if(@_){ die "cannot set values for read only accessor $accessor_func"; } return $self->__get($accessor_func); }; } } sub ro_array_accessorize{ my $class = shift; foreach my $accessor_func(@_){ no strict; #--- get my $getf = $class.'::'.$accessor_func; *$getf = sub{ my $self = shift; return $self->__get_array($accessor_func); }; } } sub array_accessorize{ my $class = shift; foreach my $accessor_func(@_){ no strict; #--- get my $getf = $class.'::get_'.$accessor_func; *$getf = sub{ my $self = shift; return $self->__get_array($accessor_func); }; #--- set my $setf = $class.'::set_'.$accessor_func; *$setf = sub{ my $self = shift; return $self->__set_array($accessor_func, @_); }; #--- add my $addf = $class.'::add_'.$accessor_func; *$addf = sub{ my $self = shift; return $self->__add_array($accessor_func, @_); }; #--- remove my $removef = $class.'::remove_'.$accessor_func; *$removef = sub{ my $self = shift; return $self->__remove_array($accessor_func, @_); }; #--- clear my $clearf = $class.'::clear_'.$accessor_func; *$clearf = sub{ my $self = shift; return $self->__clear_array($accessor_func); }; #--- default unless($class->can($accessor_func)){ my $defaultf = $class.'::'.$accessor_func; *$defaultf = sub{ my $self = shift; if(@_){ #--- with parameters, it is 'set' return $self->__set_array($accessor_func, @_); } else{ return $self->__get_array($accessor_func, @_); } }; } } } sub __get{ my $self = shift; my $func = shift; return unless ref $self; return $self->{$func}; } sub __set{ my $self = shift; my $func = shift; return unless @_; return unless ref $self; $self->{$func} = shift; return 1; } sub __get_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY'; if(@_){ if(@_ == 1){ return $self->{$func}->[shift]; } else{ return @{$self->{$func}}[@_]; } } return @{$self->{$func}}; } sub __set_array{ my $self = shift; my $func = shift; return unless @_; $self->{$func} = [@_]; 1; } sub __add_array{ my $self = shift; my $func = shift; unless(exists $self->{$func}){ $self->__set_array($func => @_); return 1; } return unless ref $self->{$func} eq 'ARRAY'; push @{$self->{$func}}, @_; 1; } sub __remove_from_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY' && @{$self->{$func}}; my $count = 0; my %bad = map {$_ => 1} @_; my $i=0; while($i < scalar(@{$self->{$func}})){ if($bad{$self->{$func}[$i]}){ splice @{$self->{$func}}, $i, 1; ++$count; next; } ++$i; } return $count; } sub __clear_array{ my $self = shift; my $func = shift; return unless exists $self->{$func} && ref $self->{$func} eq 'ARRAY' && @{$self->{$func}}; $self->{$func} = []; return; } 1; Property.pm000444023532023421 4713712121654175 16342 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Property; use warnings; use strict; require UR; use Lingua::EN::Inflect; use Class::AutoloadCAN; our $VERSION = "0.41"; # UR $VERSION;; our @CARP_NOT = qw( UR::DataSource::RDBMS UR::Object::Type ); # class_meta and r_class_meta duplicate the functionality if two properties of the same name, # but these are faster sub class_meta { return shift->{'class_name'}->class->__meta__; } sub r_class_meta { return shift->{'data_type'}->class->__meta__; } sub is_direct { my $self = shift; if ($self->is_calculated or $self->is_constant or $self->is_many or $self->via) { return 0; } return 1; } sub is_numeric { my $self = shift; unless (defined($self->{'_is_numeric'})) { my $class = $self->_data_type_as_class_name; unless ($class) { return; } $self->{'_is_numeric'} = $class->isa("UR::Value::Number"); } return $self->{'_is_numeric'}; } sub is_valid_storage_for_value { my($self, $value) = @_; my $data_class_name = $self->_data_type_as_class_name; return 1 if ($value->isa($data_class_name)); if ($data_class_name->isa('UR::Value') ) { my @underlying_types = $data_class_name->underlying_data_types; foreach my $underlying_type ( @underlying_types ) { return 1 if ($value->isa($underlying_type)); } } return 0; } sub alias_for { my $self = shift; if ($self->{'via'} and $self->{'to'} and $self->{'via'} eq '__self__') { return $self->{'to'}; } else { return $self->{'property_name'}; } } sub _convert_data_type_for_source_class_to_final_class { my ($class, $foreign_class, $source_class) = @_; $foreign_class ||= ''; # TODO: allowing "is => 'Text'" instead of is => 'UR::Value::Text' is syntactic sugar # We should have an is_primitive flag set on these so we do efficient work. my ($ns) = ($source_class =~ /^([^:]+)::/); if ($ns and not $ns->isa("UR::Namespace")) { $ns = undef; } my $final_class; if ($foreign_class) { if ($foreign_class->can('__meta__')) { $final_class = $foreign_class; } else { my ($ns_value_class, $ur_value_class); if ($ns and $ns->can("get")) { $ns_value_class = $ns . '::Value::' . $foreign_class; if ($ns_value_class->can('__meta__')) { $final_class = $ns_value_class; } } if (!$final_class) { $ur_value_class = 'UR::Value::' . $foreign_class; if ($ur_value_class->can('__meta__')) { $final_class = $ur_value_class; } } if (!$final_class) { $ur_value_class = 'UR::Value::' . ucfirst(lc($foreign_class)); if ($ur_value_class->can('__meta__')) { $final_class = $ur_value_class; } } } } if (!$final_class) { if (Class::Autouse->class_exists($foreign_class)) { return $foreign_class; } elsif ($foreign_class =~ /::/) { return $foreign_class; } else { eval "use $foreign_class;"; if (!$@) { return $foreign_class; } if (!$ns or $ns->get()->allow_sloppy_primitives) { # no colons, and no namespace: no choice but to assume it's a sloppy primitive return 'UR::Value::SloppyPrimitive'; } else { Carp::confess("Failed to find a ${ns}::Value::* or UR::Value::* module for primitive type $foreign_class!"); } } } return $final_class; } sub _data_type_as_class_name { my $self = $_[0]; return $self->{_data_type_as_class_name} ||= do { my $source_class = $self->class_name; #this is so NUMBER -> Number my $foreign_class = $self->data_type; if (not $foreign_class) { if ($self->via or $self->to) { my @joins = UR::Object::Join->resolve_chain( $self->class_name, $self->property_name, $self->property_name, ); $foreign_class = $joins[-1]->foreign_class; } } __PACKAGE__->_convert_data_type_for_source_class_to_final_class($foreign_class, $source_class); }; } # TODO: this is a method on the data source which takes a given property. # Returns the table and column for this property. # If this particular property doesn't have a column_name, and it # overrides a property defined on a parent class, then walk up the # inheritance and find the right one sub table_and_column_name_for_property { my $self = shift; # Shortcut - this property has a column_name, so the class should have the right # table_name if ($self->column_name) { return ($self->class_name->__meta__->table_name, $self->column_name); } my $property_name = $self->property_name; my @class_metas = $self->class_meta->parent_class_metas; my %seen; while (@class_metas) { my $class_meta = shift @class_metas; next if ($seen{$class_meta}++); my $p = $class_meta->property_meta_for_name($property_name); next unless $p; if ($p->column_name && $class_meta->table_name) { return ($class_meta->table_name, $p->column_name); } push @class_metas, $class_meta->parent_class_metas; } # This property has no column anywhere in the class' inheritance return; } # Return true if resolution of this property involves an ID property of # any class. sub _involves_id_property { my $self = shift; my $is_id = $self->is_id; return 1 if defined($is_id); if ($self->id_by) { my $class_meta = $self->class_meta; my $id_by_list = $self->id_by; foreach my $id_by ( @$id_by_list ) { my $id_by_meta = $class_meta->property_meta_for_name($id_by); return 1 if ($id_by_meta and $id_by_meta->_involves_id_property); } } if ($self->via) { my $via_meta = $self->via_property_meta; return 1 if ($via_meta and $via_meta->_involves_id_property); if ($self->to) { my $to_meta = $self->to_property_meta; return 1 if ($to_meta and $to_meta->_involves_id_property); if ($self->where) { unless ($to_meta) { Carp::confess("Property '" . $self->property_name . "' of class " . $self->class_name . " has 'to' metadata that does not resolve to a known property."); } my $other_class_meta = $to_meta->class_meta; my $where = $self->where; for (my $i = 0; $i < @$where; $i += 2) { my $where_meta = $other_class_meta->property_meta_for_name($where->[$i]); return 1 if ($where_meta and $where_meta->_involves_id_property); } } } } return 0; } # For via/to delegated properties, return the property meta in the same # class this property delegates through sub via_property_meta { my $self = shift; return unless ($self->is_delegated and $self->via); my $class_meta = $self->class_meta; return $class_meta->property_meta_for_name($self->via); } sub final_property_meta { my $self = shift; my $closure; $closure = sub { return unless defined $_[0]; if ($_[0]->is_delegated and $_[0]->via) { if ($_[0]->to) { return $closure->($_[0]->to_property_meta); } else { return $closure->($_[0]->via_property_meta); } } else { return $_[0]; } }; my $final = $closure->($self); return if !defined $final || $final->id eq $self->id; return $final; } # For via/to delegated properties, return the property meta on the foreign # class that this property delegates to sub to_property_meta { my $self = shift; return unless ($self->is_delegated && $self->to); my $via_meta = $self->via_property_meta(); return unless $via_meta; my $remote_class = $via_meta->data_type; # unless ($remote_class) { # # Can we guess what the data type is for multiply indirect properties? # if ($via_meta->to) { # my $to_property_meta = $via_meta->to_property_meta; # $remote_class = $to_property_meta->data_type if ($to_property_meta); # } # } return unless $remote_class; my $remote_class_meta = UR::Object::Type->get($remote_class); return unless $remote_class_meta; return $remote_class_meta->property_meta_for_name($self->to); } sub get_property_name_pairs_for_join { my ($self) = @_; unless ($self->{'_get_property_name_pairs_for_join'}) { my @linkage = $self->_get_direct_join_linkage(); unless (@linkage) { Carp::croak("Cannot resolve underlying property joins for property '" . $self->property_name . "' of class " . $self->class_name . ": Couldn't determine which properties link to the remote class"); } my @results; if ($self->reverse_as) { @results = map { [ $_->[1] => $_->[0] ] } @linkage; } else { @results = map { [ $_->[0] => $_->[1] ] } @linkage; } $self->{'_get_property_name_pairs_for_join'} = \@results; } return @{$self->{'_get_property_name_pairs_for_join'}}; } sub _get_direct_join_linkage { my ($self) = @_; my @retval; if (my $id_by = $self->id_by) { my $r_class_meta = $self->r_class_meta; unless ($r_class_meta) { Carp::croak("Property '" . $self->property_name . "' of class '" . $self->class_name . "' " . "has data_type '" . $self->data_type ."' with no class metadata"); } my @my_id_by = @{ $self->id_by }; my @their_id_by = @{ $r_class_meta->{'id_by'} }; unless (@their_id_by) { @their_id_by = ( 'id' ); } unless (@my_id_by == @their_id_by) { Carp::croak("Property '" . $self->property_name . "' of class '" . $self->class_name . "' " . "has " . scalar(@my_id_by) . " id_by elements, while its data_type (" . $self->data_type .") has " . scalar(@their_id_by)); } for (my $i = 0; $i < @my_id_by; $i++) { push @retval, [ $my_id_by[$i], $their_id_by[$i] ]; } } elsif (my $reverse_as = $self->reverse_as) { my $r_class_name = $self->data_type; @retval = $r_class_name->__meta__->property_meta_for_name($reverse_as)->_get_direct_join_linkage(); } return @retval; } sub _resolve_join_chain { my $self = shift; my $join_label = shift; $join_label ||= $self->property_name; return UR::Object::Join->resolve_chain( $self->class_name, $self->property_name, $join_label, ); } sub label_text { # The name of the property in friendly terms. my ($self,$obj) = @_; my $property_name = $self->property_name; my @words = App::Vocabulary->filter_vocabulary(map { ucfirst(lc($_)) } split(/\s+/,$property_name)); my $label = join(" ", @words); return $label; } # This gets around the need to make a custom property subclass # when a class has an attributes_have specification. # This primary example of this in base infrastructure is that # all Commands have is_input, is_output and is_param attributes. # Note: it's too permissive and will make an accessor for any hash key. # The updated code should not do this. sub CAN { my ($thisclass, $method, $self) = @_; if (ref($self)) { my $accessor_key = '_' . $method . "_accessor"; if (my $method = $self->{$accessor_key}) { return $method; } if ($self->class_name->__meta__->{attributes_have} and exists $self->class_name->__meta__->{attributes_have}{$method} ) { return $self->{$accessor_key} = sub { return $_[0]->{$method}; } } } return; } 1; =pod =head1 NAME UR::Object::Property - Class representing metadata about a class property =head1 SYNOPSIS my $prop = UR::Object::Property->get(class_name => 'Some::Class', property_name => 'foo'); my $class_meta = Some::Class->__meta__; my $prop2 = $class_meta->property_meta_for_name('foo'); # Print out the meta-property name and its value of $prop2 print map { " $_ : ".$prop2->$_ } qw(class_name property_name data_type default_value); =head1 DESCRIPTION Instances of this class represent properties of classes. For every item mentioned in the 'has' or 'id_by' section of a class definition become Property objects. =head1 INHERITANCE UR::Object::Property is a subclass of L =head1 PROPERTY TYPES For this class definition: class Some::Class { has => [ other_id => { is => 'Text' }, other => { is => 'Some::Other', id_by => 'foo_id' }, bar => { via => 'other', to => 'bar' }, foos => { is => 'Some::Foo', reverse_as => 'some', is_many => 1 }, uc_other_id => { calculate_from => 'other_id', calculate_perl => 'uc($other_id)' }, ], }; Properties generally fall in to one of these categories: =over 4 =item regular property A regular property of a class holds a single scalar. In this case, 'other_id' is a regular property. =item object accessor An object accessor property returns objects of some class. The properties of this class must link in some way with all the ID properties of the remote class (the 'is' declaration). 'other' is an object accessor property. This is how one-to-one relationships are implemented. =item via property When a class has some object accessor property, and it is helpful for an object to assumme the value of the remote class's properties, you can set up a 'via' property. In the example above, an object of this class gets the value of its 'bar' property via the 'other' object it's linked to, from that object's 'bar' property. =item reverse as or is many property This is how one-to-many relationships are implemented. In this case, the Some::Foo class must have an object accessor property called 'some', and the 'foos' property will return a list of all the Some::Foo objects where their 'some' property would have returned that object. =item calculated property A calculated property doesn't store its data directly in the object, but when its accessor is called, the calculation code is executed. =back =head1 PROPERTIES Each property has a method of the same name =head2 Direct Properties =over 4 =item class_name => Text The name of the class this Property is attached to =item property_name => Text The name of the property. The pair of class_name and property name are the ID properties of UR::Object::Property =item column_name => Text If the class is backed by a database table, then the column this property's data comes from is stored here =item data_type => Text The type of data stored in this property. Corresponds to the 'is' part of a class's property definition. =item data_length => Number The maximum size of data stored in this property =item default_value For is_optional properties, the default value given when an object is created and this property is not assigned a value. =item valid_values => ARRAY A listref of enumerated values this property may be set to =item doc => Text A place for documentation about this property =item is_id => Boolean Indicates whether this is an ID property of the class =item is_optional => Boolean Indicates whether this is property may have the value undef when the object is created =item is_transient => Boolean Indicates whether this is property is transient? =item is_constant => Boolean Indicates whether this property can be changed after the object is created. =item is_mutable => Boolean Indicates this property can be changed via its accessor. Properties cannot be both constant and mutable =item is_volatile => Boolean Indicates this property can be changed by a mechanism other than its normal accessor method. Signals are not emmitted even when it does change via its normal accessor method. =item is_classwide => Boolean Indicates this property's storage is shared among all instances of the class. When the value is changed for one instance, that change is effective for all instances. =item is_delegated => Boolean Indicates that the value for this property is not stored in the object directly, but is delegated to another object or class. =item is_calculated => Boolean Indicates that the value for this property is not a part of the object'd data directly, but is calculated in some way. =item is_transactional => Boolean Indicates the changes to the value of this property is tracked by a Context's transaction and can be rolled back if necessary. =item is_abstract => Boolean Indicates this property exists in a base class, but must be overridden in a derived class. =item is_concrete => Boolean Antonym for is_abstract. Properties cannot be both is_abstract and is_concrete, =item is_final => Boolean Indicates this property cannot be overridden in a derived class. =item is_deprecated => Boolean Indicates this property's use is deprecated. It has no effect in the use of the property in any way, but is useful in documentation. =item implied_by => Text If this property is created as a result of another property's existence, implied_by is the name of that other property. This can happen in the case where an object accessor property is defined has => [ foo => { is => 'Some::Other', id_by => 'foo_id' }, ], Here, the 'foo' property requires another property called 'foo_id', which is not explicitly declared. In this case, the Property named foo_id will have its implied_by set to 'foo'. =item id_by => ARRAY In the case of an object accessor property, this is the list of properties in this class that link to the ID properties in the remote class. =item reverse_as => Text Defines the linking property name in the remote class in the case of an is_many relationship =item via => Text For a via-type property, indicates which object accessor to go through. =item to => Text For a via-type property, indicates the property name in the remote class to get its value from. The default value is the same as property_name =item where => ARRAY Supplies additional filters for indirect properies. For example: foos => { is => 'Some::Foo', reverse_as => 'some', is_many => 1 }, blue_foos => { via => 'foos', where => [ color => 'blue' ] }, Would create a property 'blue_foos' which returns only the related Some::Foo objects that have 'blue' color. =item calculate_from => ARRAY For calculated properties, this is a list of other property names the calculation is based on =item calculate_perl => Text For calculated properties, a string containing Perl code. Any properties mentioned in calculate_from will exist in the code's scope at run time as scalars of the same name. =item class_meta => UR::Object::Type Returns the class metaobject of the class this property belongs to =back =head1 METHODS =over 4 =item via_property_meta For via/to delegated properties, return the property meta in the same class this property delegates through =item to_property_meta For via/to delegated properties, return the property meta on the foreign class that this property delegates to =back =head1 SEE ALSO UR::Object::Type, UR::Object::Type::Initializer, UR::Object =cut Value.pm000444023532023421 137312121654175 15542 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Objectpackage UR::Object::Value; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Value { is => 'UR::Value', is_abstract => 1, subclassify_by => 'entity_class_name', type_has => [ entity_class_name => { is => 'Text' }, ], has => [ rule => { is => 'UR::BoolExpr', id_by => 'id' }, entity_class_name => { via => 'rule', to => 'subject_class_name' }, ], doc => 'an unordered group of distinct UR::Objects' }; sub AUTOSUB { my ($method,$class) = @_; my $entity_class_name = $class; $entity_class_name =~ s/::Value$//g; return unless $entity_class_name; my $code = $entity_class_name->can($method); return $code if $code; } 1; Index.pm000444023532023421 4317312121654175 15561 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object# Index for cached objects. package UR::Object::Index; our $VERSION = "0.41"; # UR $VERSION;; use base qw(UR::Object); use strict; use warnings; require UR; # wrapper for one of the ID properties to make it less ugly sub indexed_property_names { no warnings; return split(/,/,$_[0]->{indexed_property_string}); } # the only non-id property has an accessor... sub data_tree { if (@_ > 1) { my $old = $_[0]->{data_tree}; my $new = $_[1]; if ($old ne $new) { $_[0]->{data_tree} = $new; $_[0]->__signal_change__('data_tree', $old, $new); } return $new; } return $_[0]->{data_tree}; } # override create to initilize the index sub create { my $class = shift; # NOTE: This is called from one location in UR::Context and relies # on all properties including the ID being specifically defined. my $self = $UR::Context::current->_construct_object($class, @_); return unless $self; $self->{data_tree} ||= {}; $self->_build_data_tree; $self->_setup_change_subscription; $self->__signal_change__("create"); return $self; } # this does a lookup as efficiently as possible sub get_objects_matching { # The hash access below generates warnings # where undef is a value. Ignore these. no warnings 'uninitialized'; my @hr = (shift->{data_tree}); my $value; for $value (@_) { my $value_ref = ref($value); if($value_ref eq "HASH") { # property => { operator => "not like", value => "H~_WGS%", escape "~" } if (my $op = $value->{operator}) { $op = lc($op); if ($op eq '=') { @hr = grep { $_ } map { $_->{$value->{'value'}} } @hr; } elsif ($op eq 'like' or $op eq 'not like') { my $not = 1 if (substr($op,0,1) eq 'n'); my $comparison_value = $value->{value}; my $escape = $value->{escape}; my $regex = UR::BoolExpr::Template::PropertyComparison::Like-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); my @thr; if ($not) { # Get the values using the regular or negative match op. foreach my $h (@hr) { foreach my $k (sort keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k !~ /$regex/) { push @thr, $h->{$k}; } } } } else { # Standard positive match for my $h (@hr) { for my $k (sort keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if ($k =~ /$regex/) { push @thr, $h->{$k}; } } } } @hr = grep { $_ } @thr; } elsif ($op eq 'in') { $value = $value->{value}; my $has_null = ( (grep { length($_) == 0 } @$value) ? 1 : 0); if ($has_null) { @hr = grep { $_ } map { @$_{@$value} } @hr; } else { my @value = grep { length($_) > 0 } @$value; @hr = grep { $_ } map { @$_{@value} } @hr; } } elsif ($op eq 'not in') { $value = $value->{value}; # make a hash if we got an array as a value #die ">@$value<" if ref($value) eq "ARRAY"; $value = { map { $_ => 1 } @$value } if ref($value) eq "ARRAY"; # if there is a single null, the not in clause will be false if ($value->{""}) { @hr = (); } else { # return everything NOT in the hash my @thr; for my $h (@hr) { for my $k (sort keys %$h) { next unless length($k); unless ($value->{$k}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } } elsif ($op eq 'true') { my @thr; foreach my $h ( @hr ) { foreach my $k ( keys %$h ) { if ($k) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif ($op eq 'false') { my @thr; foreach my $h ( @hr ) { foreach my $k ( keys %$h ) { unless ($k) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '!=') { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { # An empty string for $k means the object's value was loaded as NULL # and we want things like 0 != NULL to be true to match the SQL that # gets generated for the same rule if($k eq '' or $k != $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '>') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k > $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k < $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '>=') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k >= $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<=') { my @thr; foreach my $h (@hr) { foreach my $k (keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k <= $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq 'ne') { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { next unless $k ne ''; # an earlier undef value got saved as an empty string here if($k ne $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq '<>') { my @thr; foreach my $h (@hr) { foreach my $k (sort keys %$h) { if(length($k) and length($value->{value}) and $k ne $value->{value}) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } elsif($op eq 'between') { my @thr; my ($min,$max) = @{ $value->{value} }; foreach my $h (@hr) { foreach my $k (sort keys %$h) { if(length($k) and $k >= $min and $k <= $max) { push @thr, $h->{$k}; } } } @hr = grep { $_ } @thr; } else { use Data::Dumper; Carp::confess("Unknown operator in key-value pair used in index lookup for index " . Dumper($value)); } } else { Carp::confess("No operator specified in hashref value!" . Dumper($value)); } } elsif (not $value_ref) { # property => value @hr = grep { $_ } map { $_->{$value} } @hr; } elsif ($value_ref eq "ARRAY") { # property => [ v1, v2, v3] @hr = grep { $_ } map { @$_{@$value} } @hr; } } return (map { values(%$_) } @hr); } # private methods sub _build_data_tree { my $self = $_[0]; my @indexed_property_names = $self->indexed_property_names; my $hr_base = $self->{data_tree}; # _remove_object in bulk. %$hr_base = (); my $indexed_class_name = $self->indexed_class_name; if (my @bad_properties = grep { not $indexed_class_name->can($_) } @indexed_property_names ) { Carp::confess( "Attempt to index $indexed_class_name by properties which " . "do not function: @bad_properties" ); } # _add_object in bulk. my ($object,@values,$hr,$value); for my $object ($UR::Context::current->all_objects_loaded($indexed_class_name)) { if (@indexed_property_names) { @values = map { my $val = $object->$_; defined $val ? $val : undef } @indexed_property_names; @values = (undef) unless(@values); } $hr = $hr_base; for $value (@values) { no warnings 'uninitialized'; # in case $value is undef $hr->{$value} ||= {}; $hr = $hr->{$value}; } my $obj_id = $object->id; $hr->{$obj_id} = $object; if (Scalar::Util::isweak($UR::Context::all_objects_loaded->{$indexed_class_name}->{$obj_id})) { Scalar::Util::weaken($hr->{$obj_id}); } } } # FIXME maybe objects in an index should always be weakend? sub weaken_reference_for_object { my $self = shift; my $object = shift; my $overrides = shift; # FIXME copied from _remove_object - what's this for? no warnings; my @indexed_property_names = $self->indexed_property_names; my @values = map { ($overrides && exists($overrides->{$_})) ? $overrides->{$_} : $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr = $hr->{$value}; return unless $hr; } Scalar::Util::weaken($hr->{$object->id}); } sub _setup_change_subscription { my $self = shift; my $indexed_class_name = $self->indexed_class_name; my @indexed_property_names = $self->indexed_property_names; if (1) { # This is a new indexing strategy which pays at index creation time instead of use. my @properties_to_watch = (@indexed_property_names, qw/create delete load unload/); #print "making index $self->{id}\n"; for my $class ($indexed_class_name, @{ $UR::Object::Type::_init_subclasses_loaded{$indexed_class_name} }) { for my $property (@properties_to_watch) { my $index_list = $UR::Object::Index::all_by_class_name_and_property_name{$class}{$property} ||= []; #print " adding to $class\n"; push @$index_list, $self; } } return 1; } # This will be ignored for now. # If the __signal_change__/subscription system is improved, it may be better to go back? my %properties_to_watch = map { $_ => 1 } (@indexed_property_names, qw/create delete load unload/); $self->{_get_change_subscription} = $indexed_class_name->create_subscription( callback => sub { my ($changed_object, $changed_property, $old_value, $new_value) = @_; #print "got change $changed_property for $indexed_class_name: $changed_object->{id}: @_\n"; # ensure we don't track changes for subclasses #return() unless ref($changed_object) eq $indexed_class_name; # ensure we only add/remove for selected method calls return() unless $properties_to_watch{$_[1]}; #print "changing @_\n"; $self->_remove_object( $changed_object, { $changed_property => $old_value } ) if ($changed_property ne 'create' and $changed_property ne 'load' and $changed_property ne '__define__'); $self->_add_object($changed_object) if ($changed_property ne 'delete' and $changed_property ne 'unload'); }, note => "index monitor " . $self->id, priority => 0, ); } sub _get_change_subscription { # accessor for the change subscription $_[0]->{_get_change_subscription} = $_[1] if (@_ > 1); return $_[0]->{_get_change_subscription}; } sub _remove_object($$) { no warnings; my ($self, $object, $overrides) = @_; my @indexed_property_names = $self->indexed_property_names; my @values = map { ($overrides && exists($overrides->{$_})) ? $overrides->{$_} : $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr = $hr->{$value}; } delete $hr->{$object->id}; } sub _add_object($$) { # We get warnings when undef converts into an empty string. # For efficiency, we turn warnings off in this method. no warnings; my ($self, $object) = @_; my @indexed_property_names = $self->indexed_property_names; my @values = map { $object->$_ } @indexed_property_names; my $hr = $self->{data_tree}; my $value; for $value (@values) { $hr->{$value} ||= {}; $hr = $hr->{$value}; } $hr->{$object->id} = $object; # This is the exact formula used elsewhere. TODO: refactor, base on class meta if ($UR::Context::light_cache and substr($self->indexed_class_name,0,5) ne 'App::') { Scalar::Util::weaken($hr->{$object->id}); } } 1; =pod =head1 NAME UR::Object::Index - Indexing system for retrieving objects by non-id properties =head1 DESCRIPTION This class implements an indexing system for objects to retrieve them quickly by properties other than their ID properties. Their existence and use is managed by the Context as needed, and end-users should never need to interact with UR::Object::Index instances. Internally, they are a container for objects of the same class and a set of properties used to look them up. Each time a get() is performed on a new set of non-id properties, a new Index is created to handle the request for objects which may already exist in the object cache, The data_tree inside the Index is a multi-level hash. The levels are in the same order as the properties in the get request. At each level, the hash keys are the values that target property has. For that level and key, all the objects inside have the same value for that property. A get() by three non-id properties will have a 3-level hash. =cut Type000755023532023421 012121654175 14710 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ObjectInternalAPI.pm000444023532023421 16170312121654172 17556 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Typepackage UR::Object::Type; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION; use Sys::Hostname; use Cwd; use Scalar::Util qw(blessed); use Sub::Name; our %meta_classes; our $bootstrapping = 1; our @partially_defined_classes; our $pwd_at_compile_time = cwd(); # each method which caches data on the class for properties stores its hash key here # when properties mutate this is cleared our @cache_keys; sub property_metas { my $self = $_[0]; my @a = map { $self->property_meta_for_name($_) } $self->all_property_names(); return @a; } # Some accessor methods drawn from properties need to be overridden. # Some times because they need to operate during bootstrapping. Sometimes # because the method needs some special behavior like sorting or filtering. # Sometimes to optimize performance or cache data # This needs to remain overridden to enforce the restriction on callers sub data_source { my $self = shift; my $ds = $self->data_source_id(@_); return undef unless $ds; my $obj = UR::DataSource->get($ds) || $ds->get(); return $obj; } sub ancestry_class_metas { #my $rule_template = UR::BoolExpr::Template->resolve(__PACKAGE__,'id'); # Can't use the speed optimization of getting a template here. Using the Context to get # objects here causes endless recursion during bootstrapping map { __PACKAGE__->get($_) } shift->ancestry_class_names; #return map { $UR::Context::current->get_objects_for_class_and_rule(__PACKAGE__, $_) } # map { $rule_template->get_rule_for_values($_) } # shift->ancestry_class_names; } our $PROPERTY_META_FOR_NAME_TEMPLATE; push @cache_keys, '_property_meta_for_name'; sub property_meta_for_name { my ($self, $property_name) = @_; if (index($property_name,'.') != -1) { my @chain = split(/\./,$property_name); my $last_class_meta = $self; my $last_class_name = $self->id; my @pmeta; for my $full_link (@chain) { my ($link) = ($full_link =~ /^([^\-\?]+)/); my $property_meta = $last_class_meta->property_meta_for_name($link); push @pmeta, $property_meta; last if $link eq $chain[-1]; my @joins = UR::Object::Join->resolve_chain($last_class_name, $link); return unless @joins; $last_class_name = $joins[-1]{foreign_class}; $last_class_meta = $last_class_name->__meta__; } return unless (@pmeta and $pmeta[-1]); return @pmeta if wantarray; return $pmeta[-1]; } my $pos = index($property_name,'-'); if ($pos != -1) { $property_name = substr($property_name,0,$pos); } if (exists($self->{'_property_meta_for_name'}) and $self->{'_property_meta_for_name'}->{$property_name}) { return $self->{'_property_meta_for_name'}->{$property_name}; } $PROPERTY_META_FOR_NAME_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name'); my $property; for my $class ($self->class_name, $self->ancestry_class_names) { my $rule = $PROPERTY_META_FOR_NAME_TEMPLATE->get_rule_for_values($class, $property_name); $property = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $rule); if ($property) { return $self->{'_property_meta_for_name'}->{$property_name} = $property; } } return; } # A front-end for property_meta_for_name, but # will translate the generic 'id' property into the class' real ID property, # if it's not called 'id' sub _concrete_property_meta_for_class_and_name { my($self,$property_name) = @_; my @property_metas = $self->property_meta_for_name($property_name); for (my $i = 0; $i < @property_metas; $i++) { if ($property_metas[$i]->id eq "UR::Object\tid" and $property_name !~ /\./) #If we're looking at a foreign object's id, can't replace with our own { # This is the generic id property. Remap it to the class' real ID property name my @id_properties = $self->id_property_names; if (@id_properties == 1 and $id_properties[0] eq 'id') { next; # this class doesn't have any other ID properties } #return map { $self->_concrete_property_meta_for_class_and_name($_) } @id_properties; my @remapped = map { $self->_concrete_property_meta_for_class_and_name($_) } @id_properties; splice(@property_metas, $i, 1, @remapped); } } return @property_metas; } sub _flatten_property_name { my ($self, $name) = @_; my $flattened_name = ''; my @add_keys; my @add_values; my @meta = $self->property_meta_for_name($name); for my $meta (@meta) { my @joins = $meta->_resolve_join_chain(); for my $join (@joins) { if ($flattened_name) { $flattened_name .= '.'; } $flattened_name .= $join->{source_name_for_foreign}; if (my $where = $join->{where}) { $flattened_name .= '-' . $join->sub_group_label; my $join_class = $join->{foreign_class}; my $bx2 = UR::BoolExpr->resolve($join_class,@$where); my $bx2_flat = $bx2->flatten(); # recurses through this my ($bx2_flat_template, @values) = $bx2_flat->template_and_values(); my @keys = @{ $bx2_flat_template->{_keys} }; for my $key (@keys) { next if substr($key,0,1) eq '-'; my $full_key = $flattened_name . '?.' . $key; push @add_keys, $full_key; push @add_values, shift @values; } if (@values) { Carp:confess("Unexpected mismatch in count of keys and values!"); } } } } return ($flattened_name, \@add_keys, \@add_values); }; our $DIRECT_ID_PROPERTY_METAS_TEMPLATE; sub direct_id_property_metas { my $self = _object(shift); $DIRECT_ID_PROPERTY_METAS_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name', 'is_id >='); my $class_name = $self->class_name; my @id_property_objects = map { $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $_) } map { $DIRECT_ID_PROPERTY_METAS_TEMPLATE->get_rule_for_values($class_name, $_, 0) } @{$self->{'id_by'}}; my $sort_sub = sub ($$) { return $_[0]->is_id cmp $_[1]->is_id }; @id_property_objects = sort $sort_sub @id_property_objects; if (@id_property_objects == 0) { @id_property_objects = $self->property_meta_for_name("id"); } return @id_property_objects; } sub parent_class_names { my $self = shift; return @{ $self->{is} }; } # If $property_name represents an alias-type property (via => '__self__'), # then return a string with all the aliases removed push @cache_keys, '_resolve_property_aliases'; sub resolve_property_aliases { my($self,$property_name) = @_; return unless $property_name; unless ($self->{'_resolve_property_aliases'} && $self->{'_resolve_property_aliases'}->{$property_name}) { $self->{'_resolve_property_aliases'} ||= {}; my @property_metas = $self->property_meta_for_name($property_name); my @property_names; if (@property_metas) { @property_names = map { $_->alias_for } @property_metas; } else { # there was a problem resolving the chain of properties # This happens in the case of an object accessor (is => 'Some::Class') without an id_by my @split_names = split(/\./,$property_name); my $prop_meta = $self->property_meta_for_name(shift @split_names); return unless $prop_meta; my $foreign_class = $prop_meta->data_type && eval { $prop_meta->data_type->__meta__}; return unless $foreign_class; @property_names = ( $prop_meta->alias_for, $foreign_class->resolve_property_aliases(join('.', @split_names))); } $self->{'_resolve_property_aliases'}->{$property_name} = join('.', @property_names); } return $self->{'_resolve_property_aliases'}->{$property_name}; } push @cache_keys, '_id_property_names'; sub id_property_names { # FIXME Take a look at id_property_names and all_id_property_names. # They look extremely similar, but tests start dying if you replace one # with the other, or remove both and rely on the property's accessor method my $self = _object(shift); unless ($self->{'_id_property_names'}) { my @id_by; unless ($self->{id_by} and @id_by = @{ $self->{id_by} }) { foreach my $parent ( @{ $self->{'is'} } ) { my $parent_class = $parent->class->__meta__; next unless $parent_class; @id_by = $parent_class->id_property_names; last if @id_by; } } $self->{'_id_property_names'} = \@id_by; } return @{$self->{'_id_property_names'}}; } push @cache_keys, '_all_id_property_names'; sub all_id_property_names { # return shift->id_property_names(@_); This makes URT/t/99_transaction.t fail my $self = shift; unless ($self->{_all_id_property_names}) { my ($tmp,$last) = ('',''); $self->{_all_id_property_names} = [ grep { $tmp = $last; $last = $_; $tmp ne $_ } sort map { @{ $_->{id_by} } } map { __PACKAGE__->get($_) } ($self->class_name, $self->ancestry_class_names) ]; } return @{ $self->{_all_id_property_names} }; } sub direct_id_column_names { my $self = _object(shift); my @id_column_names = map { $_->column_name } $self->direct_id_property_metas; return @id_column_names; } sub ancestry_table_names { my $self = _object(shift); my @inherited_table_names = grep { defined($_) } map { $_->table_name } $self->ancestry_class_metas; return @inherited_table_names; } sub all_table_names { my $self = _object(shift); my @table_names = grep { defined($_) } ( $self->table_name, $self->ancestry_table_names ); return @table_names; } sub first_table_name { my $self = _object(shift); if ($self->{_first_table_name}) { return $self->{first_table_name}; } my @classes = ($self); while(@classes) { my $co = shift @classes; if (my $table_name = $co->table_name) { $self->{first_table_name} = $table_name; return $table_name; } my @parents = map { $_->__meta__ } @{$co->{'is'}}; push @classes, @parents; } return; } sub ancestry_class_names { my $self = shift; if ($self->{_ordered_inherited_class_names}) { return @{ $self->{_ordered_inherited_class_names} }; } my $ordered_inherited_class_names = $self->{_ordered_inherited_class_names} = [ @{ $self->{is} } ]; my @unchecked = @$ordered_inherited_class_names; my %seen = ( $self->{class_name} => 1 ); while (my $ancestor_class_name = shift @unchecked) { next if $seen{$ancestor_class_name}; $seen{$ancestor_class_name} = 1; my $class_meta = $ancestor_class_name->__meta__; Carp::confess("Can't find meta for $ancestor_class_name!") unless $class_meta; next unless $class_meta->{is}; push @$ordered_inherited_class_names, @{ $class_meta->{is} }; unshift @unchecked, $_ for reverse @{ $class_meta->{is} }; } return @$ordered_inherited_class_names; } push @cache_keys, '_all_property_names'; sub all_property_names { my $self = shift; if ($self->{_all_property_names}) { return @{ $self->{_all_property_names} }; } my %seen = (); my $all_property_names = $self->{_all_property_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { next if $class_name eq 'UR::Object'; my $class_meta = UR::Object::Type->get($class_name); if (my $has = $class_meta->{has}) { push @$all_property_names, grep { not exists $has->{$_}{id_by} } grep { !exists $seen{$_} } sort keys %$has; foreach (@$all_property_names) { $seen{$_} = 1; } } } return @$all_property_names; } ######################################################################## # End of overridden property methods ######################################################################## sub _resolve_meta_class_name_for_class_name { my $class = shift; my $class_name = shift; #if ($class_name->isa("UR::Object::Type") or $meta_classes{$class_name} or $class_name =~ '::Type') { if ($meta_classes{$class_name} or $class_name =~ '::Type') { return "UR::Object::Type" } else { return $class_name . "::Type"; } } sub _resolve_meta_class_name { my $class = shift; my ($rule,%extra) = UR::BoolExpr->resolve_normalized($class, @_); my %params = $rule->params_list; my $class_name = $params{class_name}; return unless $class_name; return $class->_resolve_meta_class_name_for_class_name($class_name); } # This method can go away when we have the is_cached meta-property sub first_sub_classification_method_name { my $self = shift; # This may be one of many things which class meta-data should "inherit" from classes which # its instances inherit from. This value is set to the value found on the most concrete class # in the inheritance tree. return $self->{___first_sub_classification_method_name} if exists $self->{___first_sub_classification_method_name}; $self->{___first_sub_classification_method_name} = $self->sub_classification_method_name; unless ($self->{___first_sub_classification_method_name}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{___first_sub_classification_method_name} = $parent_class->sub_classification_method_name); } } return $self->{___first_sub_classification_method_name}; } # Another thing that is "inherited" from parent class metas sub subclassify_by { my $self = shift; return $self->{'__subclassify_by'} if exists $self->{'__subclassify_by'}; $self->{'__subclassify_by'} = $self->__subclassify_by; unless ($self->{'__subclassify_by'}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{'__subclassify_by'} = $parent_class->__subclassify_by); } } return $self->{'__subclassify_by'}; } sub resolve_composite_id_from_ordered_values { my $self = shift; my $resolver = $self->get_composite_id_resolver; return $resolver->(@_); } sub resolve_ordered_values_from_composite_id { my $self = shift; my $decomposer = $self->get_composite_id_decomposer; return $decomposer->(@_); } sub get_composite_id_decomposer { my $self = shift; my $decomposer; unless ($decomposer = $self->{get_composite_id_decomposer}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $decomposer = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $decomposer = sub { if (ref($_[0])) { # ID is an arrayref, or we'll throw an exception. my $id = $_[0]; my $underlying_id_count = scalar(@$id); # Handle each underlying ID, turning each into an arrayref divided by property value. my @decomposed_ids; for my $underlying_id (@$id) { push @decomposed_ids, [map { $_ eq '' ? undef : $_ } split(/\t/,$underlying_id)]; } # Count the property values. my $underlying_property_count = scalar(@{$decomposed_ids[0]}) if @decomposed_ids; $underlying_property_count ||= 0; # Make a list of property values, but each value will be an # arrayref of a set of values instead of a single value. my @property_values; for (my $n = 0; $n < $underlying_property_count; $n++) { $property_values[$n] = [ map { $_->[$n] } @decomposed_ids ]; } return @property_values; } else { # Regular scalar ID. no warnings 'uninitialized'; # $_[0] can be undef in some cases... return split($separator,$_[0]) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_decomposer(closure)',$decomposer); $self->{get_composite_id_decomposer} = $decomposer; } return $decomposer; } sub _resolve_composite_id_separator { # TODO: make the class pull this from its parent at creation time # and only have it dump it if it differs from its parent my $self = shift; my $separator = "\t"; for my $class_meta ($self, $self->ancestry_class_metas) { if ($class_meta->composite_id_separator) { $separator = $class_meta->composite_id_separator; last; } } return $separator; } sub get_composite_id_resolver { my $self = shift; my $resolver; unless($resolver = $self->{get_composite_id_resolver}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $resolver = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $resolver = sub { if (ref($_[0]) eq 'ARRAY') { # Determine how big the arrayrefs are. my $underlying_id_count = scalar(@{$_[0]}); # We presume that, if one value is an arrayref, the others are also, # and are of equal length. my @id; for (my $id_num = 0; $id_num < $underlying_id_count; $id_num++) { # One value per id_property on the class. # Each value is an arrayref in this case. for my $value (@_) { no warnings 'uninitialized'; # Some values in the list might be undef $id[$id_num] .= $separator if $id[$id_num]; $id[$id_num] .= $value->[$id_num]; } } return \@id; } else { no warnings 'uninitialized'; # Some values in the list might be undef return join($separator,@_) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_resolver(closure)',$resolver); $self->{get_composite_id_resolver} = $resolver; } return $resolver; } # UNUSED, BUT BETTER FOR MULTI-COLUMN FK sub composite_id_list_scalar_mix { # This is like the above, but handles the case of arrayrefs # mixing with scalar values in a multi-property id. my ($self, @values) = @_; my @id_sets; for my $value (@values) { if (@id_sets == 0) { if (not ref $value) { @id_sets = ($value); } else { @id_sets = @$value; } } else { if (not ref $value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } else { for my $new_id (@$value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } } } } if (@id_sets == 1) { return $id_sets[0]; } else { return \@id_sets; } } sub id_property_sorter { # Return a closure that sort can use to sort objects by all their ID properties # This should be the same order that an SQL query with 'order by ...' would return them my $self = shift; return $self->{'_id_property_sorter'} ||= $self->sorter(); } sub sorter { #TODO: make this take +/- indications of ascending/descending #TODO: make it into a closure for speed #TODO: there are possibilities of it sorting different than a DB on mixed numbers and alpha data my ($self,@properties) = @_; push @properties, $self->id_property_names; my $key = join("__",@properties); my $sorter = $self->{_sorter}{$key}; unless ($sorter) { my @is_numeric; my @is_descending; for my $property (@properties) { if ($property =~ m/^(-|\+)(.*)$/) { push @is_descending, $1 eq '-'; $property = $2; # yes, we're manipulating the original list element } else { push @is_descending, 0; } my $class_meta; if ($self->isa("UR::Object::Set::Type")) { # If we're a set, we want to examine the property of our members. my $subject_class = $self->class_name; $subject_class =~ s/::Set$//g; $class_meta = $subject_class->__meta__;#->property($property); } else { $class_meta = $self; } my ($pmeta,@extra) = $class_meta->_concrete_property_meta_for_class_and_name($property); if(@extra) { $pmeta = $class_meta->property($property); #a composite property (typically ID) } if ($pmeta) { my $is_numeric = $pmeta->is_numeric; push @is_numeric, $is_numeric; } elsif ($UR::initialized) { Carp::cluck("Failed to find property meta for $property on $self? Cannot produce a sorter for @properties"); push @is_numeric, 0; } else { push @is_numeric, 0; } } no warnings; # don't print a warning about undef values ...alow them to be treated as 0 or '' $sorter = $self->{_sorter}{$key} ||= sub($$) { for (my $n = 0; $n < @properties; $n++) { my $property = $properties[$n]; my @property_string = split('\.',$property); my($first,$second) = $is_descending[$n] ? ($_[1], $_[0]) : ($_[0], $_[1]); for my $current (@property_string) { $first = $first->$current; $second = $second->$current; if (!defined($second)) { return -1; } elsif (!defined($first)) { return 1; } } my $cmp = $is_numeric[$n] ? $first <=> $second : $first cmp $second; return $cmp if $cmp; } return 0; }; } Sub::Name::subname("UR::Object::Type::sorter__" . $self->class_name . '__' . $key, $sorter); return $sorter; } sub is_meta { my $self = shift; my $class_name = $self->class_name; return grep { $_ ne 'UR::Object' and $class_name->isa($_) } keys %meta_classes; } sub is_meta_meta { my $self = shift; my $class_name = $self->class_name; return 1 if $meta_classes{$class_name}; return; } # Things that can't safely be removed from the object cache. our %uncachable_types = ( ( map { $_ => 0 } keys %UR::Object::Type::meta_classes), # meta-classes are locked in the cache... 'UR::Object' => 1, # .. except for UR::Object 'UR::Object::Ghost' => 0, 'UR::DataSource' => 0, 'UR::Context' => 0, 'UR::Object::Index' => 0, ); sub is_uncachable { my $self = shift; my $class_name = $self->class_name; unless (exists $uncachable_types{$class_name}) { foreach my $type ( keys %uncachable_types ) { if ($class_name->isa($type)) { $uncachable_types{$class_name} = $uncachable_types{$type}; last; } } unless (exists $uncachable_types{$class_name}) { die "Couldn't determine is_uncachable() for $class_name"; } } return $uncachable_types{$class_name}; } # Mechanisms for generating object IDs when none were specified at # creation time sub autogenerate_new_object_id_uuid { require Data::UUID; my $uuid = Data::UUID->new->create_hex(); $uuid =~ s/^0x//; return $uuid; } our $autogenerate_id_base_format = join(" ",Sys::Hostname::hostname(), "%s", time); # the %s gets $$ when needed our $autogenerate_id_iter = 10000; sub autogenerate_new_object_id_urinternal { my($self, $rule) = @_; my @id_property_names = $self->id_property_names; if (@id_property_names > 1) { # we really could, but it seems like if you # asked to do it, it _has_ to be a mistake. If there's a legitimate # reason, this check should be removed $self->error_message("Can't autogenerate ID property values for multiple ID property class " . $self->class_name); return; } return sprintf($autogenerate_id_base_format, $$) . " " . (++$autogenerate_id_iter); } sub autogenerate_new_object_id_datasource { my($self,$rule) = @_; my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { return $data_source->autogenerate_new_object_id_for_class_name_and_rule( $self->class_name, $rule ); } else { Carp::croak("Class ".$self->class." has id_generator '-datasource', but the class has no data source to delegate to"); } } # Support the autogeneration of unique IDs for objects which require them. sub autogenerate_new_object_id { my $self = _object($_[0]); #my $rule = shift; unless ($self->{'_resolved_id_generator'}) { my $id_generator = $self->id_generator; if (ref($id_generator) eq 'CODE') { $self->{'_resolved_id_generator'} = $id_generator; } elsif ($id_generator and $id_generator =~ m/^\-(\S+)/) { my $id_method = 'autogenerate_new_object_id_' . $1; my $subref = $self->can($id_method); unless ($subref) { Carp::croak("'$id_generator' is an invalid id_generator for class " . $self->class_name . ": Can't locate object method '$id_method' via package ".ref($self)); } $self->{'_resolved_id_generator'} = $subref; } else { # delegate to the data source my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { $self->{'_resolved_id_generator'} = sub { $data_source->autogenerate_new_object_id_for_class_name_and_rule( shift->class_name, shift ) }; } } } goto $self->{'_resolved_id_generator'}; } # from ::Object->generate_support_class our %support_class_suffixes = map { $_ => 1 } qw/Set View Viewer Ghost Iterator Value/; sub generate_support_class_for_extension { my $self = shift; my $extension_for_support_class = shift; my $subject_class_name = $self->class_name; unless ($subject_class_name) { Carp::confess("No subject class name for $self?"); } return unless defined $extension_for_support_class; if ($subject_class_name eq "UR::Object") { # Carp::cluck("can't generate $extension_for_support_class for UR::Object!\n"); # NOTE: we hit this a bunch of times when "getting" meta-data objects during boostrap. return; } unless ($support_class_suffixes{$extension_for_support_class}) { #$self->debug_message("Cannot generate a class with extension $extension_for_support_class."); return; } my $subject_class_obj = UR::Object::Type->get(class_name => $subject_class_name); unless ($subject_class_obj) { $self->debug_message("Cannot autogenerate $extension_for_support_class because $subject_class_name does not exist."); return; } my $new_class_name = $subject_class_name . "::" . $extension_for_support_class; my $class_obj; if ($class_obj = UR::Object::Type->is_loaded($new_class_name)) { # getting the subject class autogenerated the support class automatically # shortcut out return $class_obj; } no strict 'refs'; my @subject_parent_class_names = @{ $subject_class_name . "::ISA" }; my @parent_class_names = grep { UR::Object::Type->get(class_name => $_) } map { $_ . "::" . $extension_for_support_class } grep { $_->isa("UR::Object") } grep { $_ !~ /^UR::/ or $extension_for_support_class eq "Ghost" } @subject_parent_class_names; use strict 'refs'; unless (@parent_class_names) { if (UR::Object::Type->get(class_name => ("UR::Object::" . $extension_for_support_class))) { @parent_class_names = "UR::Object::" . $extension_for_support_class; } } unless (@parent_class_names) { #print Carp::longmess(); #$self->error_message("Cannot autogenerate $extension_for_support_class for $subject_class_name because parent classes (@subject_parent_class_names) do not have classes with that extension."); return; } my @id_property_names = $subject_class_obj->id_property_names; my %id_property_names = map { $_ => 1 } @id_property_names; if ($extension_for_support_class eq 'Ghost') { my $subject_class_metaobj = UR::Object::Type->get($self->meta_class_name); # Class object for the subject_class my %class_params = map { $_ => $subject_class_obj->$_ } grep { my $p = $subject_class_metaobj->property_meta_for_name($_) || Carp::croak("Can't no metadata for property '$_' of class ".$self->meta_class_name); ! $p->is_delegated and ! $p->is_calculated } $subject_class_obj->__meta__->all_property_names; delete $class_params{generated}; delete $class_params{meta_class_name}; delete $class_params{subclassify_by}; delete $class_params{sub_classification_meta_class_name}; delete $class_params{id_generator}; delete $class_params{id}; delete $class_params{is}; my $attributes_have = UR::Util::deep_copy($subject_class_obj->{attributes_have}); my $class_props = UR::Util::deep_copy($subject_class_obj->{has}); for (values %$class_props) { delete $_->{class_name}; delete $_->{property_name}; } %class_params = ( %class_params, class_name => $new_class_name, is => \@parent_class_names, is_abstract => 0, has => [%$class_props], attributes_have => $attributes_have, id_properties => \@id_property_names, ); $class_obj = UR::Object::Type->define(%class_params); } else { $class_obj = UR::Object::Type->define( class_name => $subject_class_name . "::" . $extension_for_support_class, is => \@parent_class_names, ); } return $class_obj; } sub has_table { my $self = shift; if ($bootstrapping) { return 0; } return 1 if $self->table_name; # FIXME - shouldn't this call inheritance() instead of parent_classes()? my @parent_classes = $self->parent_classes; for my $class_name (@parent_classes) { next if $class_name eq "UR::Object"; my $class_obj = UR::Object::Type->get(class_name => $class_name); if ($class_obj->table_name) { return 1; } } return; } sub most_specific_subclass_with_table { my $self = shift; return $self->class_name if $self->table_name; foreach my $class_name ( $self->class_name->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub most_general_subclass_with_table { my $self = shift; my @subclass_list = reverse ( $self->class_name, $self->class_name->inheritance ); foreach my $class_name ( $self->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub _load { my $class = shift; my $rule = shift; $rule = $rule->normalize; my $params = $rule->legacy_params_hash; # While core entity classes are actually loaded, # support classes dynamically generate for them as needed. # Examples are Acme::Employee::View::emp_id, and Acme::Equipment::Ghost # Try to parse the class name. my $class_name = $params->{class_name}; # See if the class autogenerates from another class. # i.e.: Acme::Foo::Bar might be generated by Acme::Foo unless ($class_name) { my $namespace = $params->{namespace}; if (my $data_source = $params->{data_source_id}) { $namespace = $data_source->get_namespace; } if ($namespace) { # FIXME This chunk seems to be getting called each time there's a new table/class #Carp::cluck("Getting all classes for namespace $namespace from the filesystem..."); my @classes = $namespace->get_material_classes; return $class->is_loaded($params); } Carp::confess("Non-class_name used to find a class object: " . join(', ', map { "$_ => " . (defined $params->{$_} ? "'" . $params->{$_} . "'" : 'undef') } keys %$params)); } # Besides the common case of asking for a class by its name, the next most # common thing is asking for multiple classes by their names. Rather than doing the # hard work of doing it "right" right here, just recursively call myself with each # item in that list if (ref $class_name eq 'ARRAY') { # FIXME is there a more efficient way to add/remove class_name from the rule? my $rule_without_class_name = $rule->remove_filter('class_name'); $rule_without_class_name = $rule_without_class_name->remove_filter('id'); # id is a synonym for class_name my @objs = map { $class->_load($rule_without_class_name->add_filter(class_name => $_)) } @$class_name; return $class->context_return(@objs); } # If the class is loaded, we're done. # This is an un-documented unique constraint right now. my $class_obj = $class->is_loaded(class_name => $class_name); return $class_obj if $class_obj; # Handle deleted classes. # This is written in non-oo notation for bootstrapping. no warnings; if ( $class_name ne "UR::Object::Type::Ghost" and UR::Object::Type::Ghost->can("class") and $UR::Context::current->get_objects_for_class_and_rule("UR::Object::Type::Ghost",$rule,0) ) { return; } # Check the filesystem. The file may create its metadata object. eval "use $class_name"; unless ($@) { # If the above module was loaded, and is an UR::Object, # this will find the object. If not, it will return nothing. $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); return $class_obj if $class_obj; } if ($@) { # We need to handle $@ here otherwise we'll see # "Can't locate UR/Object/Type/Ghost.pm in @INC" error. # We want to fall through "in the right circumstances". (my $module_path = $class_name . '.pm') =~ s/::/\//g; Carp::croak("Error while autoloading with 'use $class_name': $@") unless ($@ =~ /Can't locate $module_path in \@INC/); # FIXME: I think other conditions here will result in silent errors. } # Parse the specified class name to check for a suffix. my ($prefix, $base, $suffix) = ($class_name =~ /^([^\:]+)::(.*)::([^:]+)/); my @parts; ($prefix, @parts) = split(/::/,$class_name); for (my $suffix_pos = $#parts; $suffix_pos >= 0; $suffix_pos--) { $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); if ($class_obj) { # the class was somehow generated while we were checking other classes for it and failing. # this can happen b/c some class with a name which is a subset of the one we're looking # for might "use" the one we want. return $class_obj if $class_obj; } my $base = join("::", @parts[0 .. $suffix_pos-1]); my $suffix = join("::", @parts[$suffix_pos..$#parts]); # See if a class exists for the same name w/o the suffix. # This may cause this function to be called recursively for # classes like Acme::Equipment::Set::View::upc_code, # which would fire recursively for three extensions of # Acme::Equipment. my $full_base_class_name = $prefix . ($base ? "::" . $base : ""); my $base_class_obj = eval { $full_base_class_name->__meta__ }; if ($base_class_obj) { # If so, that class may be able to generate a support # class. $class_obj = $full_base_class_name->__extend_namespace__($suffix); if ($class_obj) { # Autogeneration worked. # We still defer to is_loaded, since other parameters # may prevent the newly "loaded" class from being # returned. return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0) } } } # If we fall-through to this point, no class was found and no module. return; } sub use_module_with_namespace_constraints { use strict; use warnings; my $self = shift; my $target_class = shift; # If you do "use Acme; $o = Acme::Rocket->new();", and Perl finds Acme.pm # at "/foo/bar/Acme.pm", Acme::Rocket must be under /foo/bar/Acme/ # in order to be dynamically loaded. my @words = split("::",$target_class); my $path; while (@words > 1) { my $namespace_name = join("::",@words[0..$#words-1]); my $namespace_expected_module = join("/",@words[0..$#words-1]) . ".pm"; if ($path = $INC{$namespace_expected_module}) { #print "got mod $namespace_expected_module at $path for $target_class\n"; $path =~ s/\/*$namespace_expected_module//g; } else { my $namespace_obj = UR::Object::Type->is_loaded(class_name => $namespace_name); if ($namespace_obj) { eval { $path = $namespace_obj->module_directory }; if ($@) { # non-module class # don't auto-use, but don't make a lot of noise about it either } } } last if $path; pop @words; } unless ($path) { #Carp::cluck("No module_directory found for namespace $namespace_name." # . " Cannot dynamically load $target_class."); return; } $self->_use_safe($target_class,$path); my $meta = UR::Object::Type->is_loaded(class_name => $target_class); if ($meta) { return $meta; } else { return; } } sub _use_safe { use strict; use warnings; my ($self, $target_class, $expected_directory) = @_; # TODO: use some smart module to determine whether the path is # relative on the current system. if (defined($expected_directory) and $expected_directory !~ /^[\/\\]/) { $expected_directory = $pwd_at_compile_time . "/" . $expected_directory; } my $class_path = $target_class . ".pm"; $class_path =~ s/\:\:/\//g; my @INC_COPY = @INC; if ($expected_directory) { unshift @INC, $expected_directory; } my $found = ""; for my $dir (@INC) { if ($dir and (-e $dir . "/" . $class_path)) { $found = $dir; last; } } if (!$found) { # not found @INC = @INC_COPY; return; } if ($expected_directory and $expected_directory ne $found) { # not found in the specified location @INC = @INC_COPY; return; } do { local $SIG{__DIE__}; local $SIG{__WARN__}; eval "use $target_class"; }; # FIXME - if the use above failed because of a compilation error in the module we're trying to # load, then the error message below just tells the user that "Compilation failed in require" # and isn't propogating the error message about what caused the compile to fail if ($@) { #local $SIG{__DIE__}; @INC = @INC_COPY; die ("ERROR DYNAMICALLY LOADING CLASS $target_class\n$@"); } for (0..$#INC) { if ($INC[$_] eq $expected_directory) { splice @INC, $_, 1; last; } } return 1; } # Create the table behind this class in the specified database. # Currently, it creates sql valid for SQLite for support of loading # up a testing DB. Maybe this should be moved somewhere under the # DataSource objects sub mk_table { my($self,$dbh) = @_; return 1 unless $self->has_table; $dbh ||= $self->dbh; my $table_name = $self->table_name(); # we only care about properties backed up by a real column my @props = grep { $_->column_name } $self->direct_property_metas(); my $sql = "create table $table_name ("; my @cols; foreach my $prop ( @props ) { my $col = $prop->column_name; my $type = $prop->data_type; my $len = $prop->data_length; my $nullable = $prop->nullable; my $string = "$col" . " " . $type; $string .= " NOT NULL" unless $nullable; push @cols, $string; } $sql .= join(',',@cols); my @id_cols = $self->direct_id_column_names(); $sql .= ", PRIMARY KEY (" . join(',',@id_cols) . ")" if (@id_cols); # Should we also check for the unique properties? $sql .= ")"; unless ($dbh->do($sql) ) { $self->error_message("Can't create table $table_name: ".$DBI::errstr."\nSQL: $sql"); return undef; } 1; } # sub _object # This is used to make sure that methods are called # as object methods and not class methods. # The typical case that's important is when something # like UR::Object::Type->method(...) is called. # If an object is expected in a method and it gets # a class instead, well, unpredictable things can # happen. # # For many methods on UR::Objects, the implementation # is in UR::Object. However, some of those methods # have the same name as methods in here (purposefully), # and those UR::Object methods often get the # UR::Object::Type object and call the same method, # which ends up in this file. The problem is when # those methods are called on UR::Object::Type # itself it come directly here, without getting # the UR::Object::Type object for UR::Object::Type # (confused yet?). So to fix this, we use _object to # make sure we have an object and not a class. # # Basically, we make sure we're working with a class # object and not a class name. # sub _object { return ref($_[0]) ? $_[0] : $_[0]->__meta__; } # new version gets everything, including "id" itself and object ref properties push @cache_keys, '_all_property_type_names'; sub all_property_type_names { my $self = shift; if ($self->{_all_property_type_names}) { return @{ $self->{_all_property_type_names} }; } #my $rule_template = UR::BoolExpr::Template->resolve('UR::Object::Type', 'id'); my $all_property_type_names = $self->{_all_property_type_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); #my $rule = $rule_template->get_rule_for_values($class_name); #my $class_meta = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Type',$rule); if (my $has = $class_meta->{has}) { push @$all_property_type_names, sort keys %$has; } } return @$all_property_type_names; } sub table_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to table_for_property') unless @_; my $property_name = shift; for my $class_object ( $self, $self->ancestry_class_metas ) { my $property_object = UR::Object::Property->get( class_name => $class_object->class_name, property_name => $property_name ); if ( $property_object ) { next unless $property_object->column_name; return $class_object->table_name; } } return; } sub column_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to column_for_property') unless @_; my $property_name = shift; my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$properties; $i++) { if ($properties->[$i] eq $property_name) { return $columns->[$i]; } } for my $class_object ( $self->ancestry_class_metas ) { my $column_name = $class_object->column_for_property($property_name); return $column_name if $column_name; } return; } sub property_for_column { my $self = _object(shift); Carp::croak('must pass a column_name to property_for_column') unless @_; my $column_name = shift; my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$columns; $i++) { if ($columns->[$i] eq $column_name) { return $properties->[$i]; } } for my $class_object ( $self->ancestry_class_metas ) { my $property_name = $class_object->property_for_column($column_name); return $property_name if $property_name; } return; } # Methods for maintaining unique constraints # This is primarily used by the class re-writer (ur update classes-from-db), but # BoolExprs use them,too # Adds a constraint by name and property list to the class metadata. The class initializer # fills this data in via the 'constraints' key, so it shouldn't call add_unique_constraint() # directly sub add_unique_constraint { my $self = shift; unless (@_) { Carp::croak('method add_unique_constraint requires a constraint name as a parameter'); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); if (exists $constraints->{$constraint_name}) { Carp::croak("A constraint named '$constraint_name' already exists for class ".$self->class_name); } unless (@_) { Carp::croak('method add_unique_constraint requires one or more property names as parameters'); } my @property_names = @_; # Add a new constraint record push @{ $self->{'constraints'} } , { sql => $constraint_name, properties => \@property_names }; # invalidate the other cached data $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } sub remove_unique_constraint { my $self = shift; unless (@_) { Carp::croak("method remove_unique_constraint requires a constraint name as a parameter"); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); unless (exists $constraints->{$constraint_name}) { Carp::croak("There is no constraint named '$constraint_name' for class ".$self->class_name); } # Remove the constraint record for (my $i = 0; $i < @{$self->{'constraints'}}; $i++) { if ($self->{'constraints'}->[$i]->{'sql'} = $constraint_name) { splice(@{$self->{'constraints'}}, $i, 1); } } $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } # This returns a list of lists. Each inner list is the properties/columns # involved in the constraint sub unique_property_sets { my $self = shift; if ($self->{_unique_property_sets}) { return @{ $self->{_unique_property_sets} }; } my $unique_property_sets = $self->{_unique_property_sets} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{constraints}) { for my $spec (@{ $class_meta->{constraints} }) { push @$unique_property_sets, [ @{ $spec->{properties} } ] } } } return @$unique_property_sets; } # Return the constraint information as a hashref # keys are the SQL constraint name, values are a listref of property/column names involved sub unique_property_set_hashref { my $self = shift; if ($self->{_unique_property_set_hashref}) { return $self->{_unique_property_set_hashref}; } my $unique_property_set_hashref = $self->{_unique_property_set_hashref} = {}; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{'constraints'}) { for my $spec (@{ $class_meta->{'constraints'} }) { my $unique_group = $spec->{'sql'}; next if ($unique_property_set_hashref->{$unique_group}); # child classes override parents $unique_property_set_hashref->{$unique_group} = [ @{$spec->{properties}} ]; } } } return $unique_property_set_hashref; } # Used by the class meta meta data constructors to make changes in the # raw data stored in the class object's hash. These should really # only matter while running ur update # Args are: # 1) An UR::Object::Property object with attribute_name, class_name, id, property_name, type_name # 2) The method called: _construct_object, load, # 3) An id? sub _property_change_callback { my($property_obj,$method, $old_val, $new_val) = @_; return if ($method eq 'load' || $method eq 'unload'); my $class_obj = UR::Object::Type->get(class_name => $property_obj->class_name); my $property_name = $property_obj->property_name; $old_val = '' unless(defined $old_val); $new_val = '' unless(defined $new_val); if ($method eq 'create') { unless ($class_obj->{'has'}->{$property_name}) { my @attr = qw( class_name data_length data_type is_delegated is_optional property_name ); my %new_property; foreach my $attr_name (@attr ) { $new_property{$attr_name} = $property_obj->$attr_name(); } $class_obj->{'has'}->{$property_name} = \%new_property; } if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'create'); } } elsif ($method eq 'delete') { if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'delete'); } delete $class_obj->{'has'}->{$property_name}; } elsif ($method eq 'is_id' and $new_val ne $old_val) { my $change = $new_val ? 'create' : 'delete'; &_id_property_change_callback($property_obj, $change); } if (exists $class_obj->{'has'}->{$property_name}->{$method}) { $class_obj->{'has'}->{$property_name}->{$method} = $new_val; } # Invalidate the cache used by all_property_names() for my $key (@cache_keys) { $class_obj->_invalidate_cached_data_for_subclasses($key); } } # Some expensive-to-calculate data gets stored in the class meta hashref # and needs to be removed for all the existing subclasses sub _invalidate_cached_data_for_subclasses { my($class_meta, @cache_keys) = @_; delete @$class_meta{@cache_keys}; my @subclasses = @{$UR::Object::Type::_init_subclasses_loaded{$class_meta->class_name}}; my %seen; while (my $subclass = shift @subclasses) { next if ($seen{$subclass}++); my $sub_meta = UR::Object::Type->get(class_name => $subclass); delete @$sub_meta{@cache_keys}; push @subclasses, @{$UR::Object::Type::_init_subclasses_loaded{$sub_meta->class_name}}; } } # A streamlined version of the method just below that dosen't check that the # data in both places is the same before a delete operation. What was happening # was that an ID property got deleted and the position checks out ok, but then # a second ID property gets deleted and now the position dosen't match because we # aren't able to update the object's position property 'cause it's an ID property # and can't be changed. # # The short story is that we've lowered the bar for making sure it's safe to delete info sub _id_property_change_callback { my $property_obj = shift; my $method = shift; return if ($method eq 'load' || $method eq 'unload'); my $class = UR::Object::Type->get(class_name => $property_obj->class_name); if ($method eq 'create') { my $pos = $property_obj->id_by; $pos += 0; # make sure it's a number if ($pos <= @{$class->{'id_by'}}) { splice(@{$class->{'id_by'}}, $pos, 0, $property_obj->property_name); } else { # $pos is past the end... probably an id property was deleted and another added push @{$class->{'id_by'}}, $property_obj->property_name; } } elsif ($method eq 'delete') { my $property_name = $property_obj->property_name; for (my $i = 0; $i < @{$class->{'id_by'}}; $i++) { if ($class->{'id_by'}->[$i] eq $property_name) { splice(@{$class->{'id_by'}}, $i, 1); return; } } #$DB::single = 1; Carp::confess("Internal data consistancy problem: could not find property named $property_name in id_by list for class meta " . $class->class_name); } else { # Shouldn't get here since ID properties can't be changed, right? #$DB::single = 1; Carp::confess("Shouldn't be here as ID properties can't change"); 1; } $class->{'_all_id_property_names'} = undef; # Invalidate the cache used by all_id_property_names } # # BOOTSTRAP CODE # sub get_with_special_parameters { my $class = shift; my $rule = shift; my %extra = @_; if (my $namespace = delete $extra{'namespace'}) { unless (keys %extra) { my @c = $namespace->get_material_classes(); @c = grep { $_->namespace eq $namespace } $class->is_loaded($rule->params_list); return $class->context_return(@c); } } return $class->SUPER::get_with_special_parameters($rule,@_); } sub __signal_change__ { my $self = shift; my @rv = $self->SUPER::__signal_change__(@_); if ($_[0] eq "delete") { my $class_name = $self->{class_name}; $self->ungenerate(); } return @rv; } our %STANDARD_VALID_SIGNALS = ( create => 1, 'delete' => 1, commit => 1, rollback => 1, load => 1, unload => 1, load_external => 1 ); sub _is_valid_signal { my $self = shift; my $aspect = shift; # Undefined attributes indicate that the subscriber wants any changes at all to generate a callback. return 1 if (! defined $aspect); # All standard creation and destruction methods emit a signal. return 1 if ($STANDARD_VALID_SIGNALS{$aspect}); for my $property ($self->all_property_names) { return 1 if $property eq $aspect; } if (!exists $self->{'_is_valid_signal'}) { $self->{'_is_valid_signal'} = { map { $_ => 1 } @{$self->{'valid_signals'}} }; } return 1 if ($self->{'_is_valid_signal'}->{$aspect}); foreach my $parent_meta ( $self->parent_class_metas ) { if ($parent_meta->_is_valid_signal($aspect)) { $self->{'_is_valid_signal'}->{$aspect} = 1; return 1; } } return 0; } sub generated { my $self = shift; if (@_) { $self->{'generated'} = shift; } return $self->{'generated'}; } sub ungenerate { my $self = shift; my $class_name = $self->class_name; delete $UR::Object::_init_subclass->{$class_name}; delete $UR::Object::Type::_inform_all_parent_classes_of_newly_loaded_subclass{$class_name}; do { no strict; no warnings; my @symbols_which_are_not_subordinate_namespaces = grep { substr($_,-2) ne '::' } keys %{ $class_name . "::" }; my $hr = \%{ $class_name . "::" }; delete @$hr{@symbols_which_are_not_subordinate_namespaces}; }; my $module_name = $class_name; $module_name =~ s/::/\//g; $module_name .= ".pm"; delete $INC{$module_name}; $self->{'generated'} = 0; } 1; AccessorWriter.pm000444023532023421 23014612121654173 20406 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type package UR::Object::Type::AccessorWriter; package UR::Object::Type; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; #use warnings FATAL => 'all'; use Carp (); use Sub::Name (); use Sub::Install (); use List::Util; sub mk_rw_accessor { my ($self, $class_name, $accessor_name, $column_name, $property_name, $is_transient) = @_; $property_name ||= $accessor_name; my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $property_name }; my $new = $_[1]; # The accessors may compare undef and an empty # string. For speed, we turn warnings off rather # than add extra code to make the warning disappear. my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $_[0]->{ $property_name } = $new; $_[0]->__signal_change__( $property_name, $old, $new ) unless $is_transient; # FIXME is $is_transient right here? Maybe is_volatile instead (if at all)? } return $new; } return $_[0]->{ $property_name }; # properties with default values are filled in at _construct_object() }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_alias_accessor { my($self, $class_name, $accessor_name, $alias_for) = @_; if ($accessor_name eq $alias_for) { Carp::croak("Cannot create alias property '$accessor_name' which is an alias to itself"); } my $full_name = join('::', $class_name, $accessor_name); my $accessor = Sub::Name::subname $full_name => sub { my $real_sub = $_[0]->can($alias_for); unless ($real_sub) { Carp::croak("Can't locate object method \"$alias_for\" via package \"$class_name\" while resolving alias property \"$accessor_name\""); } Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $real_sub, }); goto $real_sub; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_ro_accessor { my ($self, $class_name, $accessor_name, $column_name, $property_name) = @_; $property_name ||= $accessor_name; my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $property_name}; my $new = $_[1]; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { Carp::croak("Cannot change read-only property $accessor_name for class $class_name!" . " Failed to update " . $_[0]->__display_name__ . " property: $property_name from $old to $new"); } return $new; } return $_[0]->{ $property_name }; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_id_based_flex_accessor { my ($self, $class_name, $accessor_name, $id_by, $r_class_name, $where, $id_class_by) = @_; unless (ref($id_by)) { $id_by = [ $id_by ]; } my $id_resolver; my $id_decomposer; my @id; my $id; my $full_name = join( '::', $class_name, $accessor_name ); my $concrete_r_class_name = $r_class_name; my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; if (@_ == 1) { # This one is to support syntax like this # $cd->artist($different_artist); # to switch which artist object this cd points to my $object_value = shift; if ($id_class_by and not ref $object_value) { # when we have an id-class-by accessor and get a primitive, store it as a UR::Value $object_value = UR::Value->get($object_value); } if (defined $object_value) { if ($id_class_by) { $concrete_r_class_name = ($object_value->can('class') ? $object_value->class : ref($object_value)); $id_decomposer = undef; $id_resolver = undef; $self->$id_class_by($concrete_r_class_name); } elsif (! Scalar::Util::blessed($object_value) and ! $object_value->can('id')) { Carp::croak("Can't call method \"id\" without a package or object reference. Expected an object as parameter to '$accessor_name', not the value '$object_value'"); } my $r_class_meta = eval { $concrete_r_class_name->__meta__ }; unless ($r_class_meta) { Carp::croak("Can't get metadata for class $concrete_r_class_name. Is it a UR class?"); } $id_decomposer ||= $r_class_meta->get_composite_id_decomposer; @id = $id_decomposer->($object_value->id); if (@$id_by == 1) { my $id_property_name = $id_by->[0]; $self->$id_property_name($object_value->id); } else { @id = $id_decomposer->($object_value->id); Carp::croak("Cannot alter value for '$accessor_name' on $class_name: The passed-in object of type " . $object_value->class . " has " . scalar(@id) . " id properties, but the accessor '$accessor_name' has " . scalar(@$id_by) . " id_by properties"); for my $id_property_name (@$id_by) { $self->$id_property_name(shift @id); } } } else { if ($id_class_by) { $self->$id_class_by(undef); } for my $id_property_name (@$id_by) { $self->$id_property_name(undef); } } return $object_value; } else { if ($id_class_by) { $concrete_r_class_name = $self->$id_class_by; $id_decomposer = undef; $id_resolver = undef; return unless $concrete_r_class_name; } unless ($id_resolver) { my $concrete_r_class_meta = UR::Object::Type->get($concrete_r_class_name); unless ($concrete_r_class_meta) { Carp::croak("Can't resolve value for '$accessor_name' on class $class_name id '".$self->id . "': No class metadata for value '$concrete_r_class_name' referenced as property '$id_class_by'"); } $id_resolver = $concrete_r_class_meta->get_composite_id_resolver; } # eliminate the old map{} because of side effects with $_ # when the id_by property happens to be calculated #@id = map { $self->$_ } @$id_by; @id=(); for my $property_name (@$id_by) { # no implicit topic my $value = $self->$property_name; # scalar context push @id, $value; } $id = $id_resolver->(@id); return if not defined $id; if ($concrete_r_class_name eq 'UR::Object') { Carp::carp("Querying by using UR::Object class is deprecated."); } if ($concrete_r_class_name->isa("UR::Value")) { return $id; } else { if (@_ || $where) { # There were additional params passed in return $concrete_r_class_name->get(id => $id, @_, @$where); } else { return $concrete_r_class_name->get($id); } } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_id_based_object_accessor { my ($self, $class_name, $accessor_name, $id_by, $r_class_name, $where, $id_class_by) = @_; unless (ref($id_by)) { $id_by = [ $id_by ]; } my $id_resolver; my $id_decomposer; my @id; my $id; my $full_name = join( '::', $class_name, $accessor_name ); my $concrete_r_class_name = $r_class_name; my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; if (@_ == 1) { # This one is to support syntax like this # $cd->artist($different_artist); # to switch which artist object this cd points to my $object_value = shift; if (defined $object_value) { if ($id_class_by) { $concrete_r_class_name = ($object_value->can('class') ? $object_value->class : ref($object_value)); $id_decomposer = undef; $id_resolver = undef; $self->$id_class_by($concrete_r_class_name); } elsif (! Scalar::Util::blessed($object_value) and ! $object_value->can('id')) { Carp::croak("Can't call method \"id\" without a package or object reference. Expected an object as parameter to '$accessor_name', not the value '$object_value'"); } my $r_class_meta = eval { $concrete_r_class_name->__meta__ }; unless ($r_class_meta) { Carp::croak("Can't get metadata for class $concrete_r_class_name. Is it a UR class?"); } $id_decomposer ||= $r_class_meta->get_composite_id_decomposer; @id = $id_decomposer->($object_value->id); if (@$id_by == 1) { my $id_property_name = $id_by->[0]; $self->$id_property_name($object_value->id); } else { @id = $id_decomposer->($object_value->id); Carp::croak("Cannot alter value for '$accessor_name' on $class_name: The passed-in object of type " . $object_value->class . " has " . scalar(@id) . " id properties, but the accessor '$accessor_name' has " . scalar(@$id_by) . " id_by properties"); for my $id_property_name (@$id_by) { $self->$id_property_name(shift @id); } } } else { if ($id_class_by) { $self->$id_class_by(undef); } for my $id_property_name (@$id_by) { $self->$id_property_name(undef); } } return $object_value; } else { if ($id_class_by) { $concrete_r_class_name = $self->$id_class_by; $id_decomposer = undef; $id_resolver = undef; return unless $concrete_r_class_name; } unless ($id_resolver) { my $concrete_r_class_meta = UR::Object::Type->get($concrete_r_class_name); unless ($concrete_r_class_meta) { Carp::croak("Can't resolve value for '$accessor_name' on class $class_name id '".$self->id . "': No class metadata for value '$concrete_r_class_name' referenced as property '$id_class_by'"); } $id_resolver = $concrete_r_class_meta->get_composite_id_resolver; } # eliminate the old map{} because of side effects with $_ # when the id_by property happens to be calculated #@id = map { $self->$_ } @$id_by; @id=(); for my $property_name (@$id_by) { # no implicit topic my $value = $self->$property_name; # scalar context push @id, $value; } $id = $id_resolver->(@id); return if not defined $id; if ($concrete_r_class_name eq 'UR::Object') { Carp::carp("Querying by using UR::Object class is deprecated."); } if (@_ || $where) { # There were additional params passed in return $concrete_r_class_name->get(id => $id, @_, @$where); } else { return $concrete_r_class_name->get($id); } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub _resolve_bridge_logic_for_indirect_property { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where) = @_; my $bridge_collector = sub { my $self = shift; my @results = $self->$via(@$where); # Indirect has one properties must return a single undef value for an empty result, even in list context. return if @results == 1 and not defined $results[0]; return @results; }; my $bridge_crosser = sub { my $bridges = shift; return map { $_->$to(@_) } @$bridges; }; return($bridge_collector, $bridge_crosser) if ($UR::Object::Type::bootstrapping); # bail out and use the default subs if any of these fail my ($my_class_meta, $my_property_meta, $via_property_meta, $to_property_meta); $my_class_meta = $class_name->__meta__; $my_property_meta = $my_class_meta->property_meta_for_name($accessor_name) if ($my_class_meta); $via_property_meta = $my_class_meta->property_meta_for_name($via) if ($my_class_meta); $to_property_meta = $my_property_meta->to_property_meta() if ($my_property_meta); if (! $my_class_meta || ! $my_property_meta || ! $via_property_meta || ! $to_property_meta) { # Something didn't link right, use the default methods return ($bridge_collector, $bridge_crosser); } if ($my_property_meta->is_delegated and $my_property_meta->is_many and $via_property_meta->is_many and $via_property_meta->reverse_as and $via_property_meta->data_type and $via_property_meta->data_type->isa('UR::Object') ) { my $bridge_class = $via_property_meta->data_type; my @via_join_properties = eval { $via_property_meta->get_property_name_pairs_for_join }; if (! @via_join_properties) { # this can happen if the properties aren't linked together as expected. # For example, a property involved in a many-to-many relationship, but is # defined as a one-to-many with reverse_as. return ($bridge_collector, $bridge_crosser); } my (@my_join_properties,@their_join_properties); for (my $i = 0; $i < @via_join_properties; $i++) { ($my_join_properties[$i], $their_join_properties[$i]) = @{ $via_join_properties[$i] }; } my(@where_properties, @where_values); if ($where or $via_property_meta->where) { my @collected_where; @collected_where = @$where if ($where); push @collected_where, @{ $via_property_meta->where } if ($via_property_meta->where); while (@collected_where) { my $where_property = shift @collected_where; my $where_value = shift @collected_where; # FIXME Skip stuff like -hints and -order_by until UR::BE::Template->resolve() can handle them next if (substr($where_property, 0, 1) eq '-'); if (ref($where_value) eq 'HASH' and $where_value->{'operator'}) { $where_property .= ' ' .$where_value->{'operator'}; $where_value = $where_value->{'value'}; } push @where_properties, $where_property; push @where_values, $where_value; } } #my $bridge_template = UR::BoolExpr::Template->resolve($bridge_class, # @their_join_properties, # @where_properties, # -hints => [$via_property_meta->to]); my $bridge_template = UR::BoolExpr::Template->resolve($bridge_class, @their_join_properties, @where_properties); $bridge_collector = sub { my $self = shift; my @my_values = map { $self->$_} @my_join_properties; my $bx = $bridge_template->get_rule_for_values(@my_values, @where_values); return $bridge_class->get($bx); }; if($to_property_meta->is_delegated and $to_property_meta->via) { # It's a "normal" doubly delegated property my $second_via_property_meta = $to_property_meta->via_property_meta; my $final_class_name = $second_via_property_meta->data_type; if ($final_class_name and $final_class_name ne 'UR::Value' and $final_class_name->isa('UR::Object')) { my @via2_join_properties = $second_via_property_meta->get_property_name_pairs_for_join; if (@via2_join_properties > 1) { Carp::carp("via2 join not implemented :("); return; } my($my_property_name,$their_property_name) = @{ $via2_join_properties[0] }; my $crosser_template = UR::BoolExpr::Template->resolve($final_class_name, "$their_property_name in"); my $result_property_name = $to_property_meta->to; $bridge_crosser = sub { my $bridges = shift; my @linking_values = map { $_->$my_property_name } @$bridges; my $bx = $crosser_template->get_rule_for_values(\@linking_values); my @result_objects = (@_ ? $final_class_name->get($bx->params_list, @_) : $final_class_name->get($bx) ); return map { $_->$result_property_name } @result_objects; }; } } elsif ($to_property_meta->id_by and $to_property_meta->id_class_by) { # Bridging through an 'id_class_by' property # bucket the bridge items by the result class and do a get for # each of those classes with a listref of IDs my $result_class_resolver = $to_property_meta->id_class_by; my $bridging_identifiers = $to_property_meta->id_by; $bridge_crosser = sub { my $bridges = shift; my %result_class_names_and_ids; foreach my $bridge ( @$bridges ) { my $result_class = $bridge->$result_class_resolver; $result_class_names_and_ids{$result_class} ||= []; my $id_resolver = $result_class->__meta__->get_composite_id_resolver; my @id = map { $bridge->$_ } @$bridging_identifiers; my $id = $id_resolver->(@id); push @{ $result_class_names_and_ids{ $result_class } }, $id; } my @results; foreach my $result_class ( keys %result_class_names_and_ids ) { if (@_) { if($result_class->isa('UR::Value')) { #can't group queries together for UR::Values push @results, map { $result_class->get(id => $_, @_) } @{$result_class_names_and_ids{$result_class}}; } else { push @results, $result_class->get(id => $result_class_names_and_ids{$result_class}, @_); } } else { if($result_class->isa('UR::Value')) { #can't group queries together for UR::Values push @results, map { $result_class->get($_) } @{$result_class_names_and_ids{$result_class}}; } else { push @results, $result_class->get($result_class_names_and_ids{$result_class}); } } } return @results; }; } elsif ($to_property_meta->id_by and $to_property_meta->data_type and not $to_property_meta->data_type->isa('UR::Value')) { my $result_class = $to_property_meta->data_type; my $bridging_identifiers = $to_property_meta->id_by; $bridge_crosser = sub { my $bridges = shift; my @ids; foreach my $bridge ( @$bridges ) { my $id_resolver = $result_class->__meta__->get_composite_id_resolver; my @id = map { $bridge->$_ } @$bridging_identifiers; my $id = $id_resolver->(@id); push @ids, $id; } my @results = (@_ ? $result_class->get(id => \@ids, @_) : $result_class->get(\@ids) ); return @results; } } } return ($bridge_collector, $bridge_crosser); } sub _is_assignment_value { return ( @_ == 1 and not (ref($_[0]) and Scalar::Util::blessed($_[0]) and $_[0]->isa("UR::BoolExpr")) ); } sub mk_indirect_ro_accessor { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where) = @_; my @where = ($where ? @$where : ()); my $full_name = join( '::', $class_name, $accessor_name ); my $filterable_accessor_name = 'get_' . $accessor_name; # FIXME we need a better name for my $filterable_full_name = join( '::', $class_name, $filterable_accessor_name ); # This is part of an experimental refactoring of indirect accessors. The goal is to # get rid of all the special cases inside of _resolve_bridge_logic_for_indirect_property() # and do the right thing with the Join data my (@collectors, @crossers); my $accessor2 = Sub::Name::subname $full_name.'_new' => sub { my $self = shift; Carp::croak("Assignment value passed to read-only indirect accessor $accessor_name for class $class_name") if @_ and _is_assignment_value(@_); if ($class_name =~ m/^UR::/) { # Some methods will recurse into here if called on a UR::* class (especially # UR::BoolExpr), so do the dumb but safe thing my $bridge_collector = sub { my $self = shift; my @results = $self->$via(@$where); # Indirect has one properties must return a single undef value for an empty result, even in list context. return if @results == 1 and not defined $results[0]; return @results; }; #TODO: move this crosser closure logic down and get rid of the closure my @filter = @_; my $bridge_crosser = sub { return map { $_->$to(@filter) } @_ }; my @bridges = $bridge_collector->($self); return unless @bridges; return $self->context_return(@bridges) if ($to eq '-filter'); my @results = $bridge_crosser->(@bridges); return $self->context_return(@results); } unless (@collectors) { require List::MoreUtils; my $prop_meta = $class_name->__meta__->property_meta_for_name($accessor_name); my @join_list = $prop_meta->_resolve_join_chain(); foreach my $join ( @join_list ) { my @source_property_names = @{$join->{source_property_names}}; my $collector = sub { my @list = grep { defined && length } map { my $o = $_; map { $o->$_ } @source_property_names} @_; return @list == 1 ? $list[0] : \@list; }; push @collectors, $collector; my $foreign_class = $join->{foreign_class}; my $crosser; if (! $foreign_class->isa('UR::Value')) { my @foreign_property_names = @{$join->{foreign_property_names}}; $crosser = sub { my @get_params = List::MoreUtils::pairwise { $a => $b } @foreign_property_names, @_; return $foreign_class->get(@get_params); }; } push @crossers, $crosser; } } my @working = ($self); # This can probably be rewritten with List::Util::reduce for (my $i = 0; $i < @collectors; $i++) { last unless @working; my @working = $collectors[$i]->(@working); next unless $crossers[$i]; @working = $crossers[$i]->(@working); } $self->context_return(@working); }; #Sub::Install::reinstall_sub({ # into => $class_name, # as => $accessor_name.'_new', # code => $accessor2, #}); my($bridge_collector, $bridge_crosser); my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; Carp::croak("Assignment value passed to read-only indirect accessor $accessor_name for class $class_name") if @_ == 1 and _is_assignment_value(@_); unless ($bridge_collector) { ($bridge_collector, $bridge_crosser) = $ur_object_type->_resolve_bridge_logic_for_indirect_property($class_name, $accessor_name, $via, $to, \@where); } my @bridges = $bridge_collector->($self); return unless @bridges; return $self->context_return(@bridges) if ($to eq '-filter'); my @results = $bridge_crosser->(\@bridges, @_); $self->context_return(@results); }; unless ($accessor_name) { Carp::croak("No accessor name specified for read-only indirect accessor $accessor_name for class $class_name"); } Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); my $r_class_name; my $r_class_name_resolver = sub { return $r_class_name if $r_class_name; my $linking_property = UR::Object::Property->get(class_name => $class_name, property_name => $via); unless ($linking_property->data_type) { Carp::croak "Property ${class_name}::${accessor_name}: via refers to a property with no data_type. Can't process filter"; } my $final_property = UR::Object::Property->get(class_name => $linking_property->data_type, property_name => $to); unless ($final_property->data_type) { Carp::croak "Property ${class_name}::${accessor_name}: to refers to a property with no data_type. Can't process filter"; } $r_class_name = $final_property->data_type; }; my $filterable_accessor = Sub::Name::subname $filterable_full_name => sub { my $self = shift; my @results = $self->$accessor_name(); if (@_) { my $rule; if (@_ == 1 and ref($_[0]) and $_[0]->isa('UR::BoolExpr')) { $rule = shift; } else { $r_class_name ||= $r_class_name_resolver->(); $rule = UR::BoolExpr->resolve_normalized($r_class_name, @_); } @results = grep { $rule->evaluate($_) } @results; } $self->context_return(@results); }; Sub::Install::reinstall_sub({ into => $class_name, as => $filterable_accessor_name, code => $filterable_accessor, }); } sub mk_indirect_rw_accessor { my ($ur_object_type, $class_name, $accessor_name, $via, $to, $where, $singular_name) = @_; my @where = ($where ? @$where : ()); my $full_name = join( '::', $class_name, $accessor_name ); my $update_strategy; # defined the first time we "set" a value through this my $adder; my $via_property_meta; my $r_class_name; my $is_many; my $resolve_update_strategy = sub { unless (defined $update_strategy) { # Resolve the strategy. We need to figure out if $to # refers to an id-property. This is only called once, when the # accessor is first used. # If we reference a remote object, and go to one of its id properties # we must do a delete/create instead of property change. Note that # this is only allowed when the remote object has no direct properties # which are not id properties. my $my_property_meta = $class_name->__meta__->property_meta_for_name($accessor_name); unless ($my_property_meta) { Carp::croak("Failed to find property meta for '$accessor_name' on class $class_name"); } $is_many = $my_property_meta->is_many; $via_property_meta ||= $class_name->__meta__->property_meta_for_name($via); unless ($via_property_meta) { Carp::croak("Failed to find property metadata for via property '$via' while resolving property '$accessor_name' on class $class_name"); } $r_class_name ||= $via_property_meta->data_type; unless ($r_class_name) { Carp::croak("Cannot resolve property '$accessor_name' on class $class_name: It is via property '$via' which has no data_type"); } my $r_class_meta = $r_class_name->__meta__; unless ($r_class_meta) { Carp::croak("Cannot resolve property '$accessor_name' on class $class_name: It is via property '$via' with data_type $r_class_name which is not a valid class name"); } $adder = "add_" . $via_property_meta->singular_name; if ($my_property_meta->_involves_id_property) { $update_strategy = 'delete-create' } else { $update_strategy = 'change'; } } return $update_strategy; }; my ($bridge_collector, $bridge_crosser); my $accessor = Sub::Name::subname $full_name => sub { my $self = shift; unless ($bridge_collector) { ($bridge_collector, $bridge_crosser) = $ur_object_type->_resolve_bridge_logic_for_indirect_property($class_name, $accessor_name, $via, $to, \@where); } my @bridges = $bridge_collector->($self); if ( @_ == 1 and _is_assignment_value(@_) ) { $resolve_update_strategy->() unless (defined $update_strategy); if ($update_strategy eq 'change') { if (@bridges == 0) { #print "adding via $adder @where :::> $to @_\n"; @bridges = eval { $self->$adder(@where, $to => $_[0]) }; if ($@) { my $r_class_meta = $r_class_name->__meta__; my $property_meta = $r_class_meta->property($to); if ($property_meta) { # Re-throw the original exception die $@; } else { Carp::croak("Couldn't create a new object through indirect property " . "'$accessor_name' on $class_name. 'to' is $to which is not a property on $r_class_name."); } } #WAS > Carp::confess("Cannot set $accessor_name on $class_name $self->{id}: property is via $via which is not set!"); } elsif (@bridges > 1) { Carp::croak("Cannot set '$accessor_name' on $class_name id '$self->{id}': multiple instances of '$via' found, via which the property is set"); } #print "updating $bridges[0] $to to @_\n"; return $bridges[0]->$to(@_); } elsif ($update_strategy eq 'delete-create') { if (@bridges > 1) { Carp::croak("Cannot set '$accessor_name' on $class_name $self->{id}: multiple instances of '$via' found, via which the property is set"); } else { if (@bridges) { #print "deleting $bridges[0]\n"; $bridges[0]->delete; } #print "adding via $adder @where :::> $to @_\n"; @bridges = $self->$adder(@where, $to => $_[0]); unless (@bridges) { Carp::croak("Failed to add bridge for '$accessor_name' on $class_name if '$self->{id}': method $adder returned false"); } } } } if (not defined $is_many) { $resolve_update_strategy->(); } if ($is_many) { return unless @bridges; my @results = $bridge_crosser->(\@bridges, @_); $self->context_return(@results); } else { return undef unless @bridges; my @results = map { $_->$to(@_) } @bridges; $self->context_return(@results); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); if ($singular_name) { # True if we're defining an is_many indirect property # Add my $via_adder; my $add_accessor = Sub::Name::subname $class_name ."::add_$singular_name" => sub { my($self) = shift; $resolve_update_strategy->() unless (defined $update_strategy); unless (defined $via_adder) { $via_adder = "add_" . $via_property_meta->singular_name; } # By default, a single value will come in which is the remote value # we just add the appropriate property name to it. If multiple # values come in we trust the caller to be giving additional params. if (@_ == 1) { unshift @_, $to; } $self->$via_adder(@where,@_); }; Sub::Install::reinstall_sub({ into => $class_name, as => "add_$singular_name", code => $add_accessor, }); # Remove my $via_remover; my $remove_accessor = Sub::Name::subname $class_name ."::remove_$singular_name" => sub { my($self) = shift; $resolve_update_strategy->() unless (defined $update_strategy); unless (defined $via_remover) { $via_remover = "remove_" . $via_property_meta->singular_name; } # By default, a single value will come in which is the remote value # we just remove the appropriate property name to it. If multiple # values come in we trust the caller to be giving removeitional params. if (@_ == 1) { unshift @_, $to; } $self->$via_remover(@where,@_); }; Sub::Install::reinstall_sub({ into => $class_name, as => "remove_$singular_name", code => $remove_accessor, }); } } sub mk_calculation_accessor { my ($self, $class_name, $accessor_name, $calculation_src, $calculate_from, $params, $is_cached, $column_name) = @_; my $accessor; my @src; if (not defined $calculation_src or $calculation_src eq '') { $accessor = \&{ $class_name . '::' . $accessor_name }; unless ($accessor) { die "$accessor_name not defined in $class_name! Define it, or specify a calculate => sub{} or calculate => \$perl_src in the class definition."; } } elsif (ref($calculation_src) eq 'CODE') { $accessor = sub { my $self = shift; if (@_) { Carp::croak("$class_name $accessor_name is a read-only property derived from @$calculate_from"); } return $calculation_src->(map { $self->$_ } @$calculate_from); }; } elsif ($calculation_src =~ /^[^\:\W]+$/) { # built-in formula like 'sum' or 'product' my $module_name = "UR::Object::Type::AccessorWriter::" . ucfirst(lc($calculation_src)); eval "use $module_name"; die $@ if $@; @src = ( "sub ${class_name}::${accessor_name} {", 'my $self = $_[0];', "${module_name}->calculate(\$self, [" . join(",", map { "'$_'" } @$calculate_from) . "], \@_)", '}' ); } else { @src = ( "sub ${class_name}::${accessor_name} {", ($params ? 'my ($self,%params) = @_;' : 'my $self = $_[0];'), (map { "my \$$_ = \$self->$_;" } @$calculate_from), ($params ? (map { "my \$$_ = delete \$params{'$_'};" } @$params) : ()), $calculation_src, '}' ); } if (!$accessor) { if (@src) { my $src = join("\n",@src); #print ">>$src<<\n"; eval $src; if ($@) { Carp::croak "ERROR IN CALCULATED PROPERTY SOURCE: $class_name $accessor_name\n$@\n"; } $accessor = \&{ $class_name . '::' . $accessor_name }; unless ($accessor) { Cqrp::confess("Failed to generate code body for calculated property ${class_name}::${accessor_name}!"); } } else { Carp::croak "Error implementing calcuation accessor for $class_name $accessor_name!"; } } if ($accessor and $is_cached) { # Wrap the already-compiled accessor in another function to memoize the # result and save the data into the object my $calculator_sub = $accessor; $accessor = sub { if (@_ > 1) { Carp::croak("Cannot change property $accessor_name for class $class_name: cached calculated properties are read-only"); } unless (exists $_[0]->{$accessor_name}) { $_[0]->{$accessor_name} = $calculator_sub->(@_); } return $_[0]->{$accessor_name}; }; } my $full_name = join( '::', $class_name, $accessor_name ); $accessor = Sub::Name::subname $full_name => $accessor; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); return $accessor; } sub mk_dimension_delegate_accessors { my ($self, $accessor_name, $ref_class_name, $non_id_properties, $other_accessor_name, $is_transient) = @_; # Like mk_rw_accessor, but knows that this accessor is a foreign # key to a dimension table, and configures additional accessors. # Also makes this accessor "smart", to resolve the dimension # id only when needed. # Make EAV-like accessors for all of the remote properties my $class_name = $self->class_name; my $full_name = join( '::', $class_name, $other_accessor_name ); my $other_accessor = Sub::Name::subname $full_name => sub { my $self = shift; my $delegate_id = $self->{$accessor_name}; if (defined($delegate_id)) { # We're currently delegating. my $delegate = $ref_class_name->get($delegate_id); if (not @_) { # A simple get. Delegate. return $delegate->$other_accessor_name(@_); } else { # We're setting a value. # Switch from delegating to local access. # We'll switch back next-time the dimension ID # is actually requested by its accessor # (farther below). my $old = $delegate->$other_accessor_name; my $new = shift; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $self->{$accessor_name} = undef; for my $property (@$non_id_properties) { if ($property eq $other_accessor_name) { # set the value locally $self->{$property} = $new; } else { # grab the data from the (now previous) delegate $self->{$property} = $delegate->$property; } } $self->__signal_change__( $other_accessor_name, $old, $new ) unless $is_transient; return $new; } } } else { # We are not currently delegating. if (@_) { # set my $old = $self->{ $other_accessor_name }; my $new = shift; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $self->{ $other_accessor_name } = $new; $self->__signal_change__( $other_accessor_name, $old, $new ) unless $is_transient; } return $new; } else { # get return $self->{ $other_accessor_name }; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $other_accessor_name, code => $other_accessor, }); } sub mk_dimension_identifying_accessor { my ($self, $accessor_name, $ref_class_name, $non_id_properties, $is_transient) = @_; # Like mk_rw_accessor, but knows that this accessor is a foreign # key to a dimension table, and configures additional accessors. # Also makes this accessor "smart", to resolve the dimension # id only when needed. # Make EAV-like accessors for all of the remote properties my $class_name = $self->class_name; # Make the actual accessor for the id_by property my $full_name = join( '::', $class_name, $accessor_name ); my $accessor = Sub::Name::subname $full_name => sub { if (@_ > 1) { my $old = $_[0]->{ $accessor_name }; my $new = $_[1]; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { $_[0]->{ $accessor_name } = $new; $_[0]->__signal_change__( $accessor_name, $old, $new ) unless $is_transient; } return $new; } if (not defined $_[0]->{ $accessor_name }) { # Resolve an ID for the current set of values # Switch to delegating to that object. my %params; my $self = $_[0]; @params{@$non_id_properties} = delete @$self{@$non_id_properties}; my $delegate = $ref_class_name->get_or_create(%params); return undef unless $delegate; $_[0]->{ $accessor_name } = $delegate->id; } return $_[0]->{ $accessor_name }; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_rw_class_accessor { my ($self, $class_name, $accessor_name, $column_name, $variable_value) = @_; my $full_accessor_name = $class_name . "::" . $accessor_name; my $accessor = Sub::Name::subname $full_accessor_name => sub { if (@_ > 1) { $variable_value = pop; } return $variable_value; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_ro_class_accessor { my($self, $class_name, $accessor_name, $column_name, $variable_value) = @_; my $full_accessor_name = $class_name . "::" . $accessor_name; my $accessor = Sub::Name::subname $full_accessor_name => sub { if (@_ > 1) { my $old = $variable_value; my $new = $_[1]; no warnings; my $different = eval { no warnings; $old ne $new }; if ($different or $@ =~ m/has no overloaded magic/) { Carp::croak("Cannot change read-only class-wide property $accessor_name for class $class_name from $old to $new!"); } return $new; } return $variable_value; }; Sub::Install::reinstall_sub({ into => $class_name, as => $accessor_name, code => $accessor, }); } sub mk_object_set_accessors { my ($self, $class_name, $singular_name, $plural_name, $reverse_as, $r_class_name, $where) = @_; unless ($plural_name) { # TODO: we can handle a reverse_as when there is only one item. We're just not coded-to yet. Carp::croak "Bad property description for $class_name $singular_name: expected is_many with reverse_as!"; } # These are set by the resolver closure below, and kept in scope by the other closures my $rule_template; my $r_class_meta; my @property_names; my @where = ($where ? @$where : ()); my $rule_resolver = sub { my ($obj) = @_; my $loading_r_class_error = ''; if (defined $r_class_name) { eval { $r_class_meta = UR::Object::Type->is_loaded($r_class_name); unless ($r_class_meta or __PACKAGE__->use_module_with_namespace_constraints($r_class_name)) { # Don't die yet. The named class may not have a file associated with it $loading_r_class_error = "Couldn't load class $r_class_name: $@"; $@ = ''; } unless ($r_class_meta) { $r_class_name->class; $r_class_meta = UR::Object::Type->get(class_name => $r_class_name); } }; if ($@) { $loading_r_class_error .= "Couldn't get class object for $r_class_name: $@"; } } if ($r_class_meta and not $reverse_as) { # We have a real class on the other end, and it did not specify know to link back to us. # Try to infer how, otherwise fall back to the same logic we use with "primitives". my @possible_relationships = grep { $_->data_type eq $class_name } grep { defined $_->data_type } $r_class_meta->all_property_metas(); if (@possible_relationships > 1) { Carp::croak "$class_name has an ambiguous definition for property \"$singular_name\"." . " The target class $r_class_name has " . scalar(@possible_relationships) . " relationships which reference back to $class_name." . " Correct by adding \"reverse_as => X\" to ${class_name}'s \"$singular_name\" definition one of the following values: " . join(",",map { '"' . $_->delegation_name . '"' } @possible_relationships) . ".\n"; } elsif (@possible_relationships == 1) { $reverse_as = $possible_relationships[0]->property_name; } elsif (@possible_relationships == 0) { # we now fall through to the logic below and try direct arrayref storage #die "No relationships found between $r_class_name and $class_name. Error in definition for $class_name $singular_name!" } } if ($reverse_as and ! $r_class_meta) { # we've resolved reverse_as, but there's not r_class_meta?! $self->error_message("Can't resolve reverse relationship $class_name -> $plural_name. No class metadata for $r_class_name"); if ($loading_r_class_error) { Carp::croak "While loading $r_class_name: $loading_r_class_error"; } else { Carp::croak "Is class $r_class_name defined anywhere?"; } } if ($reverse_as) { # join to get the data... unless ($r_class_meta) { Carp::croak("No remote class metadata found for class $r_class_name while resolving property '$singular_name' of class $class_name"); } my $property_meta = $r_class_meta->property_meta_for_name($reverse_as); unless ($property_meta) { Carp::croak "Can't resolve reverse relationship $class_name -> $plural_name. Remote class $r_class_name has no property $reverse_as"; } my @get_params; if ($property_meta->via) { # get_property_name_pairs_for_join() only works for properties connected directly. # we still need to use it during initialization, but for more complicated relationships # this should do the right thing push @get_params, $property_meta->property_name . '.id' => $obj->id; push @property_names, 'id'; } else { my @property_links = $property_meta->get_property_name_pairs_for_join; for my $link (@property_links) { my $my_property_name = $link->[1]; push @property_names, $my_property_name; unless ($obj->can($my_property_name)) { Carp::croak "Cannot handle indirect relationship $r_class_name -> $reverse_as. Class $class_name has no property named $my_property_name"; } push @get_params, $link->[0], ($obj->$my_property_name || undef); } } if (my $id_class_by = $property_meta->id_class_by) { push @get_params, $id_class_by, $obj->class; push @property_names, 'class'; } my $tmp_rule = $r_class_name->define_boolexpr(@get_params,@where); if (my $order_by = $property_meta->order_by) { push @get_params, $order_by; } $rule_template = $tmp_rule->template; unless ($rule_template) { die "Error generating rule template to handle indirect relationship $class_name $singular_name referencing $r_class_name!"; } return $tmp_rule; } else { # data is stored locally on the hashref #die "No relationships found between $r_class_name and $class_name. Error in definition for $class_name $singular_name!" } }; my @where_values; for (my $i = 1; $i < @where; $i+=2) { if (ref($where[$i]) eq 'HASH' and exists($where[$i]->{'operator'})) { push @where_values, $where[$i]->{'value'}; # the operator is already stored in the template } else { push @where_values, $where[$i]; } } my $rule_accessor = Sub::Name::subname $class_name ."::__$singular_name" . '_rule' => sub { my $self = shift; $rule_resolver->($self) unless ($rule_template); unless ($rule_template) { die "no indirect rule available for locally-stored 'has-many' relationship"; } if (@_) { my $tmp_rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values); return $r_class_name->define_boolexpr($tmp_rule->params_list, @_); } else { return $rule_template->get_rule_for_values((map { $self->$_ } @property_names),@where_values); } }; Sub::Install::reinstall_sub({ into => $class_name, as => "__$singular_name" . '_rule', code => $rule_accessor, }); my $list_accessor = Sub::Name::subname $class_name ."::$plural_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); if (@_) { return $UR::Context::current->query($r_class_name, $rule->params_list,@_); } else { return $UR::Context::current->query($r_class_name, $rule); } } else { if (@_) { if (@_ != 1 or ref($_[0]) ne 'ARRAY' ) { die "expected a single arrayref when setting a multi-value $class_name $plural_name! Got @_"; } $self->{$plural_name} = [ @{$_[0]} ]; return @{$_[0]}; } else { return unless $self->{$plural_name}; if (ref($self->{$plural_name}) ne 'ARRAY') { Carp::carp("$class_name with id ".$self->id." does not hold an arrayref in its $plural_name property"); $self->{$plural_name} = [ $self->{$plural_name} ]; } return @{ $self->{$plural_name} }; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => $plural_name, code => $list_accessor, }); Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name . '_list', code => $list_accessor, }); my $arrayref_accessor = Sub::Name::subname $class_name ."::$singular_name" . '_arrayref' => sub { return [ $list_accessor->(@_) ]; }; Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name . '_arrayref', code => $arrayref_accessor, }); my $iterator_accessor = Sub::Name::subname $class_name ."::$singular_name" . '_iterator' => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); if (@_) { return $r_class_name->create_iterator($rule->params_list,@_); } else { return UR::Object::Iterator->create_for_filter_rule($rule); } } else { return UR::Value::Iterator->create_for_value_arrayref($self->{$plural_name} || []); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name . '_iterator', code => $iterator_accessor, }); my $set_accessor = Sub::Name::subname $class_name ."::$singular_name" . '_set' => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names),@where_values) unless (defined $rule); return $r_class_name->define_set($rule->params_list,@_); } else { # this is a bit inside-out, but works for primitives my @members = $self->$plural_name; return UR::Value->define_set(id => \@members); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name . '_set', code => $set_accessor, }); # These will behave specially if the rule does not specify the ID, or all of the ID. my @params_prefix; my $params_prefix_resolved = 0; my $params_prefix_resolver = sub { # handle the case of has-many primitives return unless $r_class_meta; my $r_ids = $r_class_meta->property_meta_for_name($reverse_as)->{id_by}; my $cmeta = UR::Object::Type->get($class_name); my $pmeta = $cmeta->{has}{$plural_name}; if (my $specify_by = $pmeta->{specify_by}) { @params_prefix = ($specify_by); } else { # TODO: should this really be an auto-setting of the specify_by meta property? my @id_property_names = $r_class_name->__meta__->id_property_names; @params_prefix = grep { my $id_property_name = $_; ( (grep { $id_property_name eq $_ } @$r_ids) ? 0 : 1) } @id_property_names; # We only do the special single-value spec when there is one property not specified by the rule. # This is common for a multi-column primary key where all columns reference a parent object, except an index value, etc. @params_prefix = () unless scalar(@params_prefix) == 1; } $params_prefix_resolved = 1; }; if ($singular_name ne $plural_name) { my $single_accessor = Sub::Name::subname $class_name ."::$singular_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $params_prefix_resolver->() unless $params_prefix_resolved; unshift @_, @params_prefix if @_ == 1; if (@_) { return my $obj = $r_class_name->get($rule->params_list,@_); } else { return my $obj = $r_class_name->get($rule); } } else { return unless $self->{$plural_name}; return unless @_; # Can't compare our list to nothing... if (@_ > 1) { Carp::croak "rule-based selection of single-item accessor not supported. Instead of single value, got @_"; } unless (ref($self->{$plural_name}) eq 'ARRAY') { Carp::croak("${class_name}::$singular_name($_[0]): $plural_name does not contain an arrayref"); } no warnings 'uninitialized'; my @matches = grep { $_ eq $_[0] } @{ $self->{$plural_name} }; return $matches[0] if @matches < 2; return $self->context_return(@matches); } }; Sub::Install::reinstall_sub({ into => $class_name, as => $singular_name, code => $single_accessor, }); } my $add_accessor = Sub::Name::subname $class_name ."::add_$singular_name" => sub { # TODO: this handles only a single item when making objects: support a list of hashrefs my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { $params_prefix_resolver->() unless $params_prefix_resolved; unshift @_, @params_prefix if @_ == 1; $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $r_class_name->create($rule->params_list,@_); } else { if ($r_class_meta) { my $obj; if (@_ == 1 and $_[0]->isa($r_class_name)) { $obj = $_[0]; } else { $obj = $r_class_name->create(@where,@_); unless ($obj) { $self->error_message("Failed to add $singular_name:" . $r_class_name->error_message); return; } } push @{ $self->{$plural_name} ||= [] }, $obj; } else { if (@_ != 1) { die "$class_name add_$singular_name expects a single value to add. Got @_"; } push @{ $self->{$plural_name} ||= [] }, $_[0]; return $_[0]; } } }; Sub::Install::reinstall_sub({ into => $class_name, as => "add_$singular_name", code => $add_accessor, }); my $remove_accessor = Sub::Name::subname $class_name ."::remove_$singular_name" => sub { my $self = shift; my $rule; $rule = $rule_resolver->($self) unless (defined $rule_template); if ($rule_template) { # an id-linked "has-many" $rule = $rule_template->get_rule_for_values((map { $self->$_ } @property_names), @where_values) unless (defined $rule); $params_prefix_resolver->() unless $params_prefix_resolved; my @matches; if (@_ == 1 and ref($_[0])) { # the object to remove was passed-in unless ($rule->evaluate($_[0])) { die "object " . $_[0]->__display_name__ . " is not a member of the $singular_name set!"; } @matches = ($_[0]); } else { # the parameters to find objects to remove were passed-in unshift @_, @params_prefix if @_ == 1; # a single "id" is the remainder of the id of the object @matches = $r_class_name->get($rule->params_list,@_); } my $trans = UR::Context::Transaction->begin; @matches = map { $_->delete or die "Error deleting $r_class_name " . $_->id . " for remove_$singular_name!: " . $_->error_message; } @matches; $trans->commit; return @matches; } else { # direct storage in an arrayref $self->{$plural_name} ||= []; if ($r_class_meta) { # object my @remove; my @keep; my $rule = $r_class_name->define_boolexpr(@_); for my $value (@{ $self->{$plural_name} }) { if ($rule->evaluate($value)) { push @keep, $value; } else { push @remove, $value; } } if (@remove) { @{ $self->{$plural_name} } = @keep; } return @remove; } else { # value (or non-ur object) if (@_ == 1) { # remove specific value my $removed; my $n = 0; for my $value (@{ $self->{$plural_name} }) { if ($value eq $_[0]) { $removed = splice(@{ $self->{$plural_name} }, $n, 1); die unless $removed eq $value; return $removed; } $n++; } die "Failed to find item @_ in $class_name $plural_name (@{$self->{$plural_name}})!"; } elsif (@_ == 0) { # remove all if no params are specified @{ $self->{$plural_name} ||= [] } = (); } else { die "$class_name remove_$singular_name should be called with a specific value. Params are only usable for ur objects! Got: @_"; } } } }; Sub::Install::reinstall_sub({ into => $class_name, as => "remove_$singular_name", code => $remove_accessor, }); } use Data::Dumper; sub initialize_direct_accessors { my $self = shift; my $class_name = $self->{class_name}; my %id_property_names; for my $property_name (@{ $self->{id_by} }) { $id_property_names{$property_name} = 1; next if $property_name eq "id"; } my %dimensions_by_fk; for my $property_name (sort keys %{ $self->{has} }) { my $property_data = $self->{has}{$property_name}; if ($property_data->{is_dimension}) { my $id_by = $property_data->{id_by}; unless ($id_by) { die "No id_by specified for dimension $property_name?"; } if (@$id_by != 1) { die "The id_by specified for dimension $property_name must list a single property name!"; } my $dimension_class_name = $property_data->{data_type}; $dimensions_by_fk{$id_by->[0]} = $dimension_class_name; my $ref_class_meta = $dimension_class_name->__meta__; my %remote_id_properties = map { $_ => 1 } $ref_class_meta->id_property_names; my @non_id_properties = grep { not $remote_id_properties{$_} } $ref_class_meta->all_property_names; for my $expected_delegate_property_name (@non_id_properties) { unless ($self->{has}{$expected_delegate_property_name}) { $self->{has}{$expected_delegate_property_name} = { $self->_normalize_property_description( $expected_delegate_property_name, { via => $property_name, to => $expected_delegate_property_name, implied_by => $property_name } ) } } } } } for my $property_name (sort keys %{ $self->{has} }) { my $property_data = $self->{has}{$property_name}; my $accessor_name = $property_name; my $column_name = $property_data->{column_name}; my $is_transient = $property_data->{is_transient}; my $where = $property_data->{where}; do { # Handle the case where the software module has an explicit # override for one of the accessors. no strict 'refs'; my $isa = \@{ $class_name . "::ISA" }; my @old_isa = @$isa; @$isa = (); if ($class_name->can($property_name)) { #warn "property $class_name $property_name exists!"; $accessor_name = "__$property_name"; } @$isa = @old_isa; }; unless ($accessor_name) { Carp::croak("No accessor name for property '$property_name' of class $class_name"); } my $accessor_type; my @calculation_fields = (qw/calculate calc_perl calc_sql calculate_from/); if (my $id_by = $property_data->{id_by}) { my $r_class_name = $property_data->{data_type}; #$self->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where); my $id_class_by = $property_data->{id_class_by}; if ($property_data->{access_as} and $property_data->{access_as} eq 'auto') { $self->mk_id_based_flex_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where, $id_class_by); $self->mk_id_based_object_accessor($class_name, $accessor_name . ($property_data->{is_many} ? '_objs' : '_obj'), $id_by, $r_class_name,$where, $id_class_by); } else { $self->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name,$where, $id_class_by); } } elsif ($property_data->{'is_calculated'} and ! $property_data->{'is_mutable'}) {# and $property_data->{'column_name'}) { # For calculated + immutable properties, their calculation function is called # by UR::Context->create_entity(), which then stores the value in the object's # hash. So, the accessor just needs to pull the data like a regular r/o accessor #$self->mk_ro_accessor($class_name, $accessor_name, $property_data->{'column_name'}); $self->mk_calculation_accessor( $class_name, $accessor_name, $property_data->{'calculate'}, $property_data->{calculate_from}, $property_data->{calculate_params}, 1, # the value should be cached $property_data->{'column_name'}, ); } elsif (my $via = $property_data->{via}) { my $to = $property_data->{to} || $property_data->{property_name}; if ($via eq '__self__') { $self->mk_alias_accessor($class_name, $accessor_name, $to); } else { if ($property_data->{is_mutable}) { my $singular_name; if ($property_data->{'is_many'}) { require Lingua::EN::Inflect; $singular_name = Lingua::EN::Inflect::PL_V($accessor_name); } $self->mk_indirect_rw_accessor($class_name,$accessor_name,$via,$to,$where,$property_data->{'is_many'} && $singular_name); } else { $self->mk_indirect_ro_accessor($class_name,$accessor_name,$via,$to,$where); } } } elsif (my $calculate = $property_data->{calculate}) { $self->mk_calculation_accessor( $class_name, $accessor_name, $property_data->{calculate}, $property_data->{calculate_from}, $property_data->{calculate_params}, $property_data->{is_constant}, $property_data->{column_name}, ); } elsif (my $calculate_sql = $property_data->{'calculate_sql'}) { # The data gets filled in by the object loader behind the scenes. # To the user, it's a read-only property $self->mk_ro_accessor($class_name, $accessor_name, $calculate_sql); } elsif ($property_data->{is_many} or $property_data->{reverse_as}){ my $reverse_as = $property_data->{reverse_as}; my $r_class_name = $property_data->{data_type}; my $singular_name; my $plural_name; if ($property_data->{is_many}) { require Lingua::EN::Inflect; $plural_name = $accessor_name; $singular_name = Lingua::EN::Inflect::PL_V($plural_name); } else { $singular_name = $accessor_name; } $self->mk_object_set_accessors($class_name, $singular_name, $plural_name, $reverse_as, $r_class_name, $where); } elsif ($property_data->{'is_classwide'}) { my $value = $property_data->{'default_value'}; if ($property_data->{'is_constant'}) { $self->mk_ro_class_accessor($class_name,$accessor_name,'',$value); } else { $self->mk_rw_class_accessor($class_name,$accessor_name,'',$value); } } else { # Just use key/value pairs in the hash for normal # table stuff, and also non-database stuff. #if ($column_name) { # push @$props, $property_name; # push @$cols, $column_name; #} my $maker; if ($id_property_names{$property_name} or not $property_data->{is_mutable}) { $maker = 'mk_ro_accessor'; } else { $maker = 'mk_rw_accessor'; } $self->$maker($class_name, $accessor_name, $column_name, $property_name,$is_transient); } } # right now we just stomp on the default accessors constructed above where they are: # 1. the fk behind a dimensional relationships # 2. the indirect properties created for the dimensional relationship for my $dimension_id (keys %dimensions_by_fk) { my $dimension_class_name = $dimensions_by_fk{$dimension_id}; my $ref_class_meta = $dimension_class_name->__meta__; my %remote_id_properties = map { $_ => 1 } $ref_class_meta->id_property_names; my @non_id_properties = grep { not $remote_id_properties{$_} } $ref_class_meta->all_property_names; for my $added_property_name (@non_id_properties) { $self->mk_dimension_delegate_accessors($dimension_id,$dimension_class_name, \@non_id_properties, $added_property_name); } $self->mk_dimension_identifying_accessor($dimension_id,$dimension_class_name, \@non_id_properties); } return 1; } 1; =pod =head1 NAME UR::Object::Type::AccessorWriter - Helper module for UR::Object::Type responsible for creating accessors for properties =head1 DESCRIPTION Subroutines within this module actually live in the UR::Object::Type namespace; this module is just a convienent place to collect them. The class initializer uses these subroutines when it's time to create accessor methods for a newly defined class. Each accessor is implemented by a closure that is then assigned a name by Sub::Name and inserted into the defined class's namespace by Sub::Install. =head1 METHODS =over 4 =item initialize_direct_accessors $classobj->initialize_direct_accessors(); This is the entry point into the accessor writing system. It inspects each item in the 'has' key of the class object's hashref, and creates methods for each property. =item mk_rw_accessor $classobj->mk_rw_accessor($class_name, $accessor_name, $column_name, $property_name, $is_transient); Creates a mutable accessor named $accessor_name which stores its value in the $property_name key of the object's hashref. =item mk_ro_accessor $classobj->mk_ro_accessor($class_name, $accessor_name, $column_name, $property_name); Creates a read-only accessor named $accessor_name which retrieves its value in the $property_name key of the object's hashref. If the method is used as a mutator by passing in a value to the method, it will throw an exception with Carp::croak. =item mk_id_based_object_accessor $classobj->mk_id_based_object_accessor($class_name, $accessor_name, $id_by, $r_class_name, $where); Creates an object accessor named $accessor_name. It returns objects of type $r_class_name, id-ed by the parameters named in the $id_by arrayref. $where is an optional listref of additional filters to apply when retrieving objects. The behavior of the created accessor depends on the number of parameters passed to it. For 0 params, it retrieves the object pointed to by $r_class_name and $id_by. For 1 param, it looks up the ID param values of the passed-in object-parameter, and reassigns value stored in the $id_by properties of the acted-upon object, effectively acting as a mutator. For more than 1 param, the additional parameters are taken as properties/values to filter the returned objects on =item mk_indirect_ro_accessor $classobj->mk_indirect_ro_accessor($class_name, $accessor_name, $via, $to, $where); Creates a read-only via accessor named $accessor_name. Its value is obtained by calling the object accessor named $via, and then calling the method $to on that object. The optional $where listref is used as additional filters when calling $via. =item mk_indirect_rw_accessor $classobj->mk_indirect_rw_accessor($class_name, $accessor_name, $via, $to, $where, $singular_name); Creates a via accessor named $accessor_name that is able to change the property it points to with $to when called as a mutator. If the $to property on the remote object is an ID property of its class, it deletes the refered-to object and creates a new one with the appropriate properties. Otherwise, it updates the $to property on the refered-to object. =item mk_calculation_accessor $classobj->mk_calculation_accessor($class_name, $accessor_name, $calculation_src, $calculate_from, $params, $is_constant, $column_name); Creates a calculated accessor called $accessor_name. If the $is_constant flag is true, then the accessor runs the calculation once, caches the result, and returns that result for subseqent calls to the accessor. $calculation_src can be one of: coderef, string containing Perl code, or the name of a module under UR::Object::Type::AccessorWriter which has a method called C. If $calculation_src is empty, then $accessor_name must be the name of an already-existing subroutine in the class's namespace. =item mk_dimension_delegate_accessors =item mk_dimension_identifying_accessor These create accessors for dealing with dimension tables in OLAP-type schemas. They need more documentation. =item mk_rw_class_accessor $classobj->mk_rw_class_accessor($class_name, $accessor_name, $column_name, $variable_value); Creates a read-write accessor called $accessor_name which stores its value in a scalar captured by the accessor's closure. Since the closure is inserted into the class's namespace, all instances of the class share the same closure (and therefore the same scalar), and the property effectively acts as a class-wide property. =item mk_ro_class_accessor $classobj->mk_ro_class_accessor($class_name, $accessor_name, $column_name, $variable_value); Creates a read-only accessor called $accessor_name which retrieves its value from a scalar captured by the accessor's closure. The value is initialized to $variable_value. If called as a mutator, it throws an exception through Carp::croak =back =head1 SEE ALSO UR::Object::Type::AccessorWriter, UR::Object::Type =cut Initializer.pm000444023532023421 21107012121654174 17726 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type # This line forces correct deployment by some tools. package UR::Object::Type::Initializer; package UR::Object::Type; use strict; use warnings; require UR; BEGIN { # Perl 5.10 did not require mro in order to call get_mro but it looks # like that was "fixed" in newer version. if ($^V ge v5.9.5) { eval "require mro"; } }; our $VERSION = "0.41"; # UR $VERSION; use Carp (); use Sub::Name (); use Sub::Install (); # keys are class property names (like er_role, is_final, etc) and values are # the default value to use if it's not specified in the class definition # # For most classes, this kind of thing is handled by the default_value attribute on # a class' property. For bootstrapping reasons, the default values for the # properties of UR::Object::Type' class need to be listed here as well. If # any of these change, or new default valued items are added, be sure to also # update the class definition for UR::Object::Type (which really lives in UR.pm # for the moment) %UR::Object::Type::defaults = ( er_role => 'entity', is_final => 0, is_singleton => 0, is_transactional => 1, is_mutable => 1, is_many => 0, is_abstract => 0, subclassify_by_version => 0, ); # All those same comments also apply to UR::Object::Property's properties %UR::Object::Property::defaults = ( is_optional => 0, is_transient => 0, is_constant => 0, is_volatile => 0, is_classwide => 0, is_delegated => 0, is_calculated => 0, is_mutable => undef, is_transactional => 1, is_many => 0, is_numeric => 0, is_specified_in_module_header => 0, is_deprecated => 0, position_in_module_header => -1, doc_position => -1, is_undocumented => 0, ); @UR::Object::Type::meta_id_ref_shared_properties = ( qw/ is_optional is_transient is_constant is_volatile is_classwide is_transactional is_abstract is_concrete is_final is_many is_deprecated is_undocumented / ); %UR::Object::Type::converse = ( required => 'optional', abstract => 'concrete', one => 'many', ); # These classes are used to define an object class. # As such, they get special handling to bootstrap the system. our %meta_classes = map { $_ => 1 } qw/ UR::Object UR::Object::Type UR::Object::Property /; our $bootstrapping = 1; our @partially_defined_classes; # When copying the object hash to create its db_committed, these keys should be removed because # they contain things like coderefs our @keys_to_delete_from_db_committed = qw( id db_committed _id_property_sorter get_composite_id_resolver get_composite_id_decomposer ); # Stages of Class Initialization # # define() is called to indicate the class structure (create() may also be called by the db sync command to make new classes) # # the parameters to define()/create() are normalized by _normalize_class_description() # # a basic functional class meta object is created by _define_minimal_class_from_normalized_class_description() # # accessors are created # # if we're still bootstrapping: # # the class is stashed in an array so the post-bootstrapping stages can be done in bulk # # we exit define() # # if we're done bootstrapping: # # _inform_all_parent_classes_of_newly_loaded_subclass() sets up an internal map of known subclasses of each base class # # _complete_class_meta_object_definitions() decomposes the definition into normalized objects # sub __define__ { my $class = shift; my $desc = $class->_normalize_class_description(@_); my $class_name = $desc->{class_name} ||= (caller(0))[0]; $desc->{class_name} = $class_name; my $self; my %params = $class->_construction_params_for_desc($desc); my $meta_class_name; if (%params) { $self = __PACKAGE__->__define__(%params); return unless $self; $meta_class_name = $params{class_name}; } else { $meta_class_name = __PACKAGE__; } $self = $UR::Context::all_objects_loaded->{$meta_class_name}{$class_name}; if ($self) { #$DB::single = 1; #Carp::cluck("Re-defining class $class_name? Found $meta_class_name with id '$class_name'"); return $self; } $self = $class->_make_minimal_class_from_normalized_class_description($desc); Carp::confess("Failed to define class $class_name!") unless $self; # we do this for define() but not create() my %db_committed = %$self; delete @db_committed{@keys_to_delete_from_db_committed}; $self->{'db_committed'} = \%db_committed; $self->_initialize_accessors_and_inheritance or Carp::confess("Error initializing accessors for $class_name!"); if ($bootstrapping) { push @partially_defined_classes, $self; } else { unless ($self->_inform_all_parent_classes_of_newly_loaded_subclass()) { Carp::confess( "Failed to link to parent classes to complete definition of class $class_name!" . $class->error_message ); } unless ($self->_complete_class_meta_object_definitions()) { #$DB::single = 1; $self->_complete_class_meta_object_definitions(); Carp::confess( "Failed to complete definition of class $class_name!" . $class->error_message ); } } return $self; } sub create { # this is typically only used by code which intendes to autogenerate source code # it will lead to the writing of a Perl module upon commit. my $class = shift; my $desc = $class->_normalize_class_description(@_); my $class_name = $desc->{class_name} ||= (caller(0))[0]; my $meta_class_name = $desc->{meta_class_name}; no strict 'refs'; unless ( $meta_class_name eq __PACKAGE__ or # in newer Perl interpreters the ->isa() call can return true # even if @ISA has been emptied (OS X) ??? (scalar(@{$meta_class_name . '::ISA'}) and $meta_class_name->isa(__PACKAGE__)) ) { if (__PACKAGE__->get(class_name => $meta_class_name)) { warn "class $meta_class_name already exists when creating class meta for $class_name?!"; } else { __PACKAGE__->create( __PACKAGE__->_construction_params_for_desc($desc) ); } } my $self = $class->_make_minimal_class_from_normalized_class_description($desc); Carp::confess("Failed to define class $class_name!") unless $self; $self->_initialize_accessors_and_inheritance or Carp::confess("Failed to define class $class_name!"); $self->_inform_all_parent_classes_of_newly_loaded_subclass() or Carp::confess( "Failed to link to parent classes to complete definition of class $class_name!" . $class->error_message ); $self->generated(0); $self->__signal_change__("create"); return $self; } sub _preprocess_subclass_description { # allow a class to modify the description of any subclass before it instantiates # this filtering allows a base class to specify policy, add meta properties, etc. my ($self,$prev_desc) = @_; my $current_desc = $prev_desc; if (my $preprocessor = $self->subclass_description_preprocessor) { # the preprocessor must me a method name in the class being adjusted no strict 'refs'; unless ($self->class_name->can($preprocessor)) { die "Class " . $self->class_name . " specifies a pre-processor for subclass descriptions " . $preprocessor . " which is not defined in the " . $self->class_name . " package!"; } $current_desc = $self->class_name->$preprocessor($current_desc); $current_desc = $self->_normalize_class_description_impl(%$current_desc); } # only call it on the direct parent classes, let recursion walk the tree my @parent_class_names = grep { $_->can('__meta__') } $self->parent_class_names(); for my $parent_class_name (@parent_class_names) { my $parent_class = $parent_class_name->__meta__; $current_desc = $parent_class->_preprocess_subclass_description($current_desc); } return $current_desc; } sub _construction_params_for_desc { my $class = shift; my $desc = shift; my $class_name = $desc->{class_name}; my $meta_class_name = $desc->{meta_class_name}; my @extended_metadata; if ($desc->{type_has}) { @extended_metadata = ( has => [ @{ $desc->{type_has} } ] ); } if ( $meta_class_name eq __PACKAGE__ ) { if (@extended_metadata) { die "Cannot extend class metadata of $class_name because it is a class involved in UR bootstrapping."; } return(); } else { if ($bootstrapping) { return ( class_name => $meta_class_name, is => __PACKAGE__, @extended_metadata, ); } else { my $parent_classes = $desc->{is}; my @meta_parent_classes = map { $_ . '::Type' } @$parent_classes; for (@$parent_classes) { __PACKAGE__->use_module_with_namespace_constraints($_); eval {$_->class}; if ($@) { die "Error with parent class $_ when defining $class_name! $@"; } } return ( class_name => $meta_class_name, is => \@meta_parent_classes, @extended_metadata, ); } } } sub initialize_bootstrap_classes { # This is called once at the end of compiling the UR module set to handle # classes which did incomplete initialization while bootstrapping. # Until bootstrapping occurs is done, my $class = shift; for my $class_meta (@partially_defined_classes) { unless ($class_meta->_inform_all_parent_classes_of_newly_loaded_subclass) { my $class_name = $class_meta->{class_name}; Carp::confess ( "Failed to complete inheritance linkage definition of class $class_name!" . $class_meta->error_message ); } } while (my $class_meta = shift @partially_defined_classes) { unless ($class_meta->_complete_class_meta_object_definitions()) { my $class_name = $class_meta->{class_name}; Carp::confess( "Failed to complete definition of class $class_name!" . $class_meta->error_message ); } } $bootstrapping = 0; # It should be safe to set up callbacks now. # __define__ instead of create() so a subsequent rollback won't remove the observer # and since we're in bootstrapping time, we have to supply an ID. The UUID generator # doesn't require any outside info, so it's safe to use UR::Observer->__define__(id => UR::Object::Type->autogenerate_new_object_id_uuid, subject_class_name => 'UR::Object::Property', callback => \&UR::Object::Type::_property_change_callback); } sub _normalize_class_description { my $class = shift; my $desc = $class->_normalize_class_description_impl(@_); unless ($bootstrapping) { for my $parent_class_name (@{ $desc->{is} }) { my $parent_class = $parent_class_name->__meta__; $desc = $parent_class->_preprocess_subclass_description($desc); } } # we previously handled property meta extensions when normalizing the property # now we merely save unrecognized things # this is now done afterward so that parent classes can preprocess their subclasses descriptions before extending # normalize the data behind the property descriptions my @property_names = keys %{$desc->{has}}; for my $property_name (@property_names) { my $pdesc = $desc->{has}->{$property_name}; my $unknown_ma = delete $pdesc->{unrecognized_meta_attributes}; next unless $unknown_ma; for my $name (keys %$unknown_ma) { if (exists $desc->{attributes_have}->{$name}) { $pdesc->{$name} = delete $unknown_ma->{$name}; } } if (%$unknown_ma) { my $class_name = $desc->{class_name}; my @unknown_ma = sort keys %$unknown_ma; Carp::confess("unknown meta-attributes present for $class_name $property_name: @unknown_ma\n"); } } return $desc; } sub _normalize_class_description_impl { my $class = shift; my %old_class = @_; if (exists $old_class{extra}) { $DB::single=1; %old_class = (%{delete $old_class{extra}}, %old_class); } my $class_name = delete $old_class{class_name}; my %new_class = ( class_name => $class_name, is_singleton => $UR::Object::Type::defaults{'is_singleton'}, is_final => $UR::Object::Type::defaults{'is_final'}, is_abstract => $UR::Object::Type::defaults{'is_abstract'}, ); for my $mapping ( [ class_name => qw//], [ type_name => qw/english_name/], [ is => qw/inheritance extends isa is_a/], [ is_abstract => qw/abstract/], [ is_final => qw/final/], [ is_singleton => qw//], [ is_transactional => qw//], [ id_by => qw/id_properties/], [ has => qw/properties/], [ type_has => qw//], [ attributes_have => qw//], [ er_role => qw/er_type/], [ doc => qw/description/], [ relationships => qw//], [ constraints => qw/unique_constraints/], [ namespace => qw//], [ schema_name => qw//], [ data_source_id => qw/data_source instance/], [ table_name => qw/sql dsmap/], [ select_hint => qw/query_hint/], [ join_hint => qw//], [ subclassify_by => qw/sub_classification_property_name/], [ sub_classification_meta_class_name => qw//], [ sub_classification_method_name => qw//], [ first_sub_classification_method_name => qw//], [ composite_id_separator => qw//], [ generate => qw//], [ generated => qw//], [ subclass_description_preprocessor => qw//], [ id_generator => qw/id_sequence_generator_name/], [ subclassify_by_version => qw//], [ meta_class_name => qw//], [ valid_signals => qw//], ) { my ($primary_field_name, @alternate_field_names) = @$mapping; my @all_fields = ($primary_field_name, @alternate_field_names); my @values = grep { defined($_) } delete @old_class{@all_fields}; if (@values > 1) { Carp::confess( "Multiple values in class definition for $class_name for field " . join("/", @all_fields) ); } elsif (@values == 1) { $new_class{$primary_field_name} = $values[0]; } } if (my $pp = $new_class{subclass_description_preprocessor}) { if (!ref($pp)) { unless ($pp =~ /::/) { # a method name, not fully qualified $new_class{subclass_description_preprocessor} = $new_class{class_name} . '::' . $new_class{subclass_description_preprocessor}; } else { $new_class{subclass_description_preprocessor} = $pp; } } elsif (ref($pp) ne 'CODE') { die "unexpected " . ref($pp) . " reference for subclass_description_preprocessor for $class_name!"; } } unless ($new_class{er_role}) { $new_class{er_role} = $UR::Object::Type::defaults{'er_role'}; } my @crap = qw/source/; delete @old_class{@crap}; if ($class_name =~ /^(.*?)::/) { $new_class{namespace} = $1; } else { $new_class{namespace} = $new_class{class_name}; } if (not exists $new_class{is_transactional} and not $meta_classes{$class_name} ) { $new_class{is_transactional} = $UR::Object::Type::defaults{'is_transactional'}; } unless ($new_class{is}) { no warnings; no strict 'refs'; if (my @isa = @{ $class_name . "::ISA" }) { $new_class{is} = \@isa; } } unless ($new_class{is}) { if ($new_class{table_name}) { $new_class{is} = ['UR::Entity'] } else { $new_class{is} = ['UR::Object'] } } unless ($new_class{'doc'}) { $new_class{'doc'} = undef; } if ($new_class{'valid_signals'}) { if (!ref($new_class{'valid_signals'})) { # If it's a plain string, wrap it into an arrayref $new_class{'valid_signals'} = [ $new_class{'valid_signals'} ]; } elsif (ref($new_class{'valid_signals'}) ne 'ARRAY') { Carp::confess("The 'valid_signals' metadata for class $class_name must be an arrayref"); } } else { $new_class{'valid_signals'} = []; } for my $field (qw/is id_by has relationships constraints/) { next unless exists $new_class{$field}; my $reftype = ref($new_class{$field}); if (! $reftype) { # It's a plain string, wrap it in an arrayref $new_class{$field} = [ $new_class{$field} ]; } elsif ($reftype eq 'HASH') { # Later code expects it to be a listref - convert it my @params_as_list; foreach my $attr_name ( keys (%{$new_class{$field}}) ) { push @params_as_list, $attr_name; push @params_as_list, $new_class{$field}->{$attr_name}; } $new_class{$field} = \@params_as_list; } elsif ($reftype ne 'ARRAY') { die "Class $class_name cannot initialize because its $field section is not a string, arrayref or hashref"; } } # These may have been found and moved over. Restore. $old_class{has} = delete $new_class{has}; $old_class{attributes_have} = delete $new_class{attributes_have}; # Install structures to track fully formatted property data. my $instance_properties = $new_class{has} = {}; my $meta_properties = $new_class{attributes_have} = {}; # The id might be a single value, or not specified at all. my $id_properties; if (not exists $new_class{id_by}) { if ($new_class{is}) { $id_properties = $new_class{id_by} = []; } else { $id_properties = $new_class{id_by} = [ id => { is_optional => 0 } ]; } } elsif ( (not ref($new_class{id_by})) or (ref($new_class{id_by}) ne 'ARRAY') ) { $id_properties = $new_class{id_by} = [ $new_class{id_by} ]; } else { $id_properties = $new_class{id_by}; } # Transform the id properties into a list of raw ids, # and move the property definitions into "id_implied" # where present so they can be processed below. my $property_rank = 0; do { my @replacement; my $pos = 0; for (my $n = 0; $n < @$id_properties; $n++) { my $name = $id_properties->[$n]; my $data = $id_properties->[$n+1]; if (ref($data)) { $old_class{id_implied}->{$name} ||= $data; if (my $obj_ids = $data->{id_by}) { push @replacement, (ref($obj_ids) ? @$obj_ids : ($obj_ids)); } else { push @replacement, $name; } $n++; } else { $old_class{id_implied}->{$name} ||= {}; push @replacement, $name; } $old_class{id_implied}->{$name}->{'position_in_module_header'} = $pos++; } @$id_properties = @replacement; }; if (@$id_properties > 1 and grep {$_ eq 'id'} @$id_properties) { Carp::croak("Cannot initialize class $class_name: " . "Cannot have an ID property named 'id' when the class has multiple ID properties (" . join(', ', map { "'$_'" } @$id_properties) . ")"); } # Flatten and format the property list(s) in the class description. # NOTE: we normalize the details at the end of normalizing the class description. my @keys = grep { /has|attributes_have/ } keys %old_class; unshift @keys, qw(id_implied); # we want to hit this first to preserve position_ and is_specified_ keys foreach my $key ( @keys ) { # parse the key to see if we're looking at instance or meta attributes, # and take the extra words as additional attribute meta-data. my @added_property_meta; my $properties; if ($key =~ /has/) { @added_property_meta = grep { $_ ne 'has' } split(/[_-]/,$key); $properties = $instance_properties; } elsif ($key =~ /attributes_have/) { @added_property_meta = grep { $_ ne 'attributes' and $_ ne 'have' } split(/[_-]/,$key); $properties = $meta_properties; } elsif ($key eq 'id_implied') { # these are additions to the regular "has" list from complex identity properties $properties = $instance_properties; } else { die "Odd key $key?"; } @added_property_meta = map { 'is_' . $_ => 1 } @added_property_meta; # the property data can be a string, array, or hash as they come in # convert string, hash and () into an array my $property_data = delete $old_class{$key}; my @tmp; if (!ref($property_data)) { if (defined($property_data)) { @tmp = split(/\s+/, $property_data); } else { @tmp = (); } } elsif (ref($property_data) eq 'HASH') { @tmp = map { ($_ => $property_data->{$_}) } sort keys %$property_data; } elsif (ref($property_data) eq 'ARRAY') { @tmp = @$property_data; } else { die "Unrecognized data $property_data appearing as property list!"; } # process the array of property specs my $pos = 0; while (my $name = shift @tmp) { my $params; if (ref($tmp[0])) { $params = shift @tmp; unless (ref($params) eq 'HASH') { my $seen_type = ref($params); Carp::confess("class $class_name property $name has a $seen_type reference instead of a hashref describing its meta-attributes!"); } %$params = (@added_property_meta, %$params) if @added_property_meta; } else { $params = { @added_property_meta }; } unless (exists $params->{'position_in_module_header'}) { $params->{'position_in_module_header'} = $pos++; } unless (exists $params->{is_specified_in_module_header}) { $params->{is_specified_in_module_header} = $class_name . '::' . $key; } # Indirect properties can mention the same property name more than once. To # avoid stomping over existing property data with this other property data, # merge the new info into the existing hash. Otherwise, the new property name # gets an empty hash of info if ($properties->{$name}) { # this property already exists, but is also implied by some other property which added it to the end of the listed # extend the existing definition foreach my $key ( keys %$params ) { next if ($key eq 'is_specified_in_module_header' || $key eq 'position_in_module_header'); # once a property gets set to is_optional => 0, it stays there, even if it's later set to 1 next if ($key eq 'is_optional' and exists($properties->{$name}->{'is_optional'}) and defined($properties->{$name}->{'is_optional'}) and $properties->{$name}->{'is_optional'} == 0); $properties->{$name}->{$key} = $params->{$key}; } $params = $properties->{$name}; } else { $properties->{$name} = $params; } # a single calculate_from can be a simple string, convert to a listref if (my $calculate_from = $params->{'calculate_from'}) { $params->{'calculate_from'} = [ $calculate_from ] unless (ref($calculate_from) eq 'ARRAY'); } if (my $id_by = $params->{id_by}) { $id_by = [ $id_by ] unless ref($id_by) eq 'ARRAY'; my @id_by_names; while (@$id_by) { my $id_name = shift @$id_by; my $params2; if (ref($id_by->[0])) { $params2 = shift @$id_by; } else { $params2 = {}; } for my $p (@UR::Object::Type::meta_id_ref_shared_properties) { if (exists $params->{$p}) { $params2->{$p} = $params->{$p}; } } $params2->{implied_by} = $name; $params2->{is_specified_in_module_header} = 0; push @id_by_names, $id_name; push @tmp, $id_name, $params2; } $params->{id_by} = \@id_by_names; } if (my $id_class_by = $params->{'id_class_by'}) { if (ref $id_class_by) { Carp::croak("Cannot initialize class $class_name: " . "Property $name has an 'id_class_by' that is not a plain string"); } push @tmp, $id_class_by, { implied_by => $name, is_specified_in_module_header => 0 }; } } # next property in group # id-by properties' metadata can influence the id-ed-by property metadata for my $pdata (values %$properties) { next unless $pdata->{id_by}; for my $id_property (@{ $pdata->{id_by} }) { my $id_pdata = $properties->{$id_property}; for my $p (@UR::Object::Type::meta_id_ref_shared_properties) { if (exists $id_pdata->{$p} xor exists $pdata->{$p}) { # if one or the other specifies a value, copy it to the one that's missing $id_pdata->{$p} = $pdata->{$p} = $id_pdata->{$p} || $pdata->{$p}; } elsif (!exists $id_pdata->{$p} and !exists $pdata->{$p} and exists $UR::Object::Property::defaults{$p}) { # if neither has a value, use the default for both $id_pdata->{$p} = $pdata->{$p} = $UR::Object::Property::defaults{$p}; } } } } } # next group of properties # NOT ENABLED YET if (0) { # done processing direct properties of this process # extend %$instance_properties with properties of the parent classes my @parent_class_names = @{ $new_class{is} }; for my $parent_class_name (@parent_class_names) { my $parent_class_meta = $parent_class_name->__meta__; die "no meta for $parent_class_name while initializing $class_name?" unless $parent_class_meta; my $parent_normalized_properties = $parent_class_meta->{has}; for my $parent_property_name (keys %$parent_normalized_properties) { my $parent_property_data = $parent_normalized_properties->{$parent_property_name}; my $inherited_copy = $instance_properties->{$parent_property_name}; unless ($inherited_copy) { $inherited_copy = UR::Util::deep_copy($parent_property_data); } $inherited_copy->{class_name} = $class_name; my $override = $inherited_copy->{overrides_class_names} ||= []; push @$override, $parent_property_data->{class_name}; } } } if (($new_class{data_source_id} and not ref($new_class{data_source_id})) and not $new_class{schema_name}) { my $s = $new_class{data_source_id}; $s =~ s/^.*::DataSource:://; $new_class{schema_name} = $s; } if (%old_class) { # this should have all been deleted above # we actually process it later, since these may be related to parent classes extending # the class definition $new_class{extra} = \%old_class; }; # ensure parent classes are loaded unless ($bootstrapping) { my @base_classes = map { ref($_) ? @$_ : $_ } $new_class{is}; for my $parent_class_name (@base_classes) { # ensure the parent classes are fully processed no warnings; unless ($parent_class_name->can("__meta__")) { __PACKAGE__->use_module_with_namespace_constraints($parent_class_name); Carp::croak("Class $class_name cannot initialize because of errors using parent class $parent_class_name: $@") if $@; } unless ($parent_class_name->can("__meta__")) { if ($ENV{'HARNESS_ACTIVE'}) { Carp::confess("Class $class_name cannot initialize because of errors using parent class $parent_class_name. Failed to find static method '__meta__' on $parent_class_name. Does class $parent_class_name exist, and is it loaded?\n The entire list of base classes was ".join(', ', @base_classes)); } Carp::croak("Class $class_name cannot initialize because of errors using parent class $parent_class_name. Failed to find static method '__meta__' on $parent_class_name. Does class $parent_class_name exist, and is it loaded?"); } my $parent_class = $parent_class_name->__meta__; unless ($parent_class) { Carp::carp("No class metadata object for $parent_class_name"); next; } # the the parent classes indicate version, if needed if ($parent_class->{'subclassify_by_version'} and not $parent_class_name =~ /::Ghost/) { unless ($class_name =~ /^${parent_class_name}::V\d+/) { my $ns = $parent_class_name; $ns =~ s/::.*//; my $version; if ($ns and $ns->can("component_version")) { $version = $ns->component_version($class); } unless ($version) { $version = '1'; } $parent_class_name = $parent_class_name . '::V' . $version; eval "use $parent_class_name"; Carp::confess("Error using versioned module $parent_class_name!:\n$@") if $@; redo; } } } $new_class{is} = \@base_classes; } # normalize the data behind the property descriptions my @property_names = keys %$instance_properties; for my $property_name (@property_names) { my %old_property = %{ $instance_properties->{$property_name} }; my %new_property = $class->_normalize_property_description1($property_name, \%old_property, \%new_class); %new_property = $class->_normalize_property_description2(\%new_property, \%new_class); $instance_properties->{$property_name} = \%new_property; } # allow parent classes to adjust the description in systematic ways my $desc = \%new_class; my @additional_property_meta_attributes; unless ($bootstrapping) { for my $parent_class_name (@{ $new_class{is} }) { my $parent_class = $parent_class_name->__meta__; if (my $parent_meta_properties = $parent_class->{attributes_have}) { push @additional_property_meta_attributes, %$parent_meta_properties; } } } # Find 'via' properties where the to is '-filter' and rewrite them to # copy some attributes from the source property # This feels like a hack, but it makes other parts of the system easier by # not having to deal with -filter foreach my $property_name ( @property_names ) { my $property_data = $instance_properties->{$property_name}; if ($property_data->{'to'} && $property_data->{'to'} eq '-filter') { my $via = $property_data->{'via'}; my $via_property_data = $instance_properties->{$via}; unless ($via_property_data) { Carp::croak "Cannot initialize class $class_name: Property '$property_name' filters '$via', but there is no property '$via'."; } $property_data->{'data_type'} = $via_property_data->{'data_type'}; $property_data->{'reverse_as'} = $via_property_data->{'reverse_as'}; if ($via_property_data->{'where'}) { unshift @{$property_data->{'where'}}, @{$via_property_data->{'where'}}; } } } # Catch a mistake in the class definition where a property is 'via' # something, and its 'to' is the same as the via's reverse_as. This # ends up being a circular definition and generates junk SQL foreach my $property_name ( @property_names ) { my $property_data = $instance_properties->{$property_name}; my $via = $property_data->{'via'}; my $to = $property_data->{'to'}; if (defined($via) and defined($to)) { my $via_property_data = $instance_properties->{$via}; next unless ($via_property_data and $via_property_data->{'reverse_as'}); if ($via_property_data->{'reverse_as'} eq $to) { Carp::croak("Cannot initialize class $class_name: Property '$property_name' defines " . "an incompatible relationship. Its 'to' is the same as reverse_as for property '$via'"); } } } unless ($bootstrapping) { # cascade extra meta attributes from the parent downward for my $parent_class_name (@{ $new_class{is} }) { my $parent_class = $parent_class_name->__meta__; if (my $parent_meta_properties = $parent_class->{attributes_have}) { #push @additional_property_meta_attributes, %$parent_meta_properties; } } %$meta_properties = (%$meta_properties, @additional_property_meta_attributes); # Inheriting from an abstract class that subclasses with a subclassify_by means that # this class' property named by that subclassify_by is actually a constant equal to this # class' class name PARENT_CLASS: foreach my $parent_class_name ( @{ $new_class{'is'} }) { my $parent_class_meta = $parent_class_name->__meta__(); foreach my $ancestor_class_meta ( $parent_class_meta->all_class_metas ) { if (my $subclassify_by = $ancestor_class_meta->subclassify_by) { if (not $instance_properties->{$subclassify_by}) { my %old_property = ( property_name => $subclassify_by, default_value => $class_name, is_constant => 1, is_classwide => 1, is_specified_in_module_header => 0, column_name => '', implied_by => $parent_class_meta->class_name . '::subclassify_by', ); my %new_property = $class->_normalize_property_description1($subclassify_by, \%old_property, \%new_class); my %new_property2 = $class->_normalize_property_description2(\%new_property, \%new_class); $instance_properties->{$subclassify_by} = \%new_property2; last PARENT_CLASS; } } } } } my $meta_class_name = __PACKAGE__->_resolve_meta_class_name_for_class_name($class_name); $desc->{meta_class_name} ||= $meta_class_name; return $desc; } sub _normalize_property_description1 { my $class = shift; my $property_name = shift; my $property_data = shift; my $class_data = shift || $class; my $class_name = $class_data->{class_name}; my %old_property = %$property_data; my %new_class = %$class_data; if (exists $old_property{unrecognized_meta_attributes}) { %old_property = (%{delete $old_property{unrecognized_meta_attributes}}, %old_property); } delete $old_property{source}; if ($old_property{implied_by} and $old_property{implied_by} eq $property_name) { $class->warning_message("Cleaning up odd self-referential 'implied_by' on $class_name $property_name"); delete $old_property{implied_by}; } # Only 1 of is_abstract, is_concrete or is_final may be set { no warnings 'uninitialized'; my $modifier_sum = $old_property{is_abstract} + $old_property{is_concrete} + $old_property{is_final}; if ($modifier_sum > 1) { Carp::confess("abstract/concrete/final are mutually exclusive. Error in class definition for $class_name property $property_name!"); } elsif ($modifier_sum == 0) { $old_property{is_concrete} = 1; } } my %new_property = ( class_name => $class_name, property_name => $property_name, ); for my $mapping ( [ property_type => qw/resolution/], [ class_name => qw//], [ property_name => qw//], [ column_name => qw/sql/], [ constraint_name => qw//], [ data_length => qw/len/], [ data_type => qw/type is isa is_a/], [ default_value => qw/default value/], [ valid_values => qw//], [ example_values => qw//], [ doc => qw/description/], [ is_optional => qw/is_nullable nullable optional/], [ is_transient => qw//], [ is_volatile => qw//], [ is_constant => qw//], [ is_classwide => qw/is_class_wide/], [ is_delegated => qw//], [ is_calculated => qw//], [ is_mutable => qw//], [ is_transactional => qw//], [ is_abstract => qw//], [ is_concrete => qw//], [ is_final => qw//], [ is_many => qw//], [ is_deprecated => qw//], [ is_undocumented => qw//], [ is_numeric => qw//], [ is_id => qw//], [ id_by => qw//], [ id_class_by => qw//], [ specify_by => qw//], [ order_by => qw//], [ access_as => qw//], [ via => qw//], [ to => qw//], [ where => qw/restrict filter/], [ implied_by => qw//], [ calculate => qw//], [ calculate_from => qw//], [ calculate_perl => qw/calc_perl/], [ calculate_sql => qw/calc_sql/], [ calculate_js => qw//], [ reverse_as => qw/reverse_id_by im_its/], [ is_legacy_eav => qw//], [ is_dimension => qw//], [ is_specified_in_module_header => qw//], [ position_in_module_header => qw//], [ singular_name => qw//], [ plural_name => qw//], ) { my $primary_field_name = $mapping->[0]; my $found_key; foreach my $key ( @$mapping ) { if (exists $old_property{$key}) { if ($found_key) { my @keys = grep { exists $old_property{$_} } @$mapping; Carp::croak("Invalid class definition for $class_name in property '$property_name'. The keys " . join(', ',$found_key,@keys) . " are all synonyms for $primary_field_name"); } $found_key = $key; } } if ($found_key) { $new_property{$primary_field_name} = delete $old_property{$found_key}; } elsif (exists $UR::Object::Property::defaults{$primary_field_name}) { $new_property{$primary_field_name} = $UR::Object::Property::defaults{$primary_field_name}; } } if (my $data = delete $old_property{delegate}) { if ($data->{via} =~ /^eav_/ and $data->{to} eq 'value') { $new_property{is_legacy_eav} = 1; } else { die "Odd delegation for $property_name: " . Data::Dumper::Dumper($data); } } if ($new_property{data_type}) { if (my ($length) = ($new_property{data_type} =~ /\((\d+)\)$/)) { $new_property{data_length} = $length; $new_property{data_type} =~ s/\(\d+\)$//; } if ($new_property{data_type} =~ m/[^\w:]/) { Carp::croak("Can't initialize class $class_name: Property '" . $new_property{property_name} . "' has metadata for is/data_type that does not look like a class name ($new_property{data_type})"); } } if (%old_property) { $new_property{unrecognized_meta_attributes} = \%old_property; %new_property = (%old_property, %new_property); } return %new_property; } sub _normalize_property_description2 { my $class = shift; my $property_data = shift; my $class_data = shift || $class; my $property_name = $property_data->{property_name}; my $class_name = $property_data->{class_name}; my %new_property = %$property_data; my %new_class = %$class_data; if (grep { $_ ne 'is_calculated' && /calc/ } keys %new_property) { $new_property{is_calculated} = 1; } if ($new_property{via} || $new_property{to} || $new_property{id_by} || $new_property{reverse_as} ) { $new_property{is_delegated} = 1; if (defined $new_property{via} and not defined $new_property{to}) { $new_property{to} = $property_name; } } if (!defined($new_property{is_mutable})) { if ($new_property{is_delegated} or (defined $class_data->{'subclassify_by'} and $class_data->{'subclassify_by'} eq $property_name) ) { $new_property{is_mutable} = 0; } else { $new_property{is_mutable} = 1; } } # For classes that have (or pretend to have) tables, the Property objects # should get their column_name property automatically filled in my $the_data_source; if (ref($new_class{'data_source_id'}) eq 'HASH') { # This is an inline-defined data source $the_data_source = $new_class{'data_source_id'}->{'is'}; } elsif ($new_class{'data_source_id'}) { $the_data_source = $new_class{'data_source_id'}; $the_data_source = UR::DataSource->get($the_data_source) || eval { $the_data_source->get() }; unless ($the_data_source) { Carp::croak("Can't resolve data source from value '".$new_class{'data_source_id'}."' in class definition for $class_name"); } } # UR::DataSource::File-backed classes don't have table_names, but for querying/saving to # work property, their properties still have to have column_name filled in if (($new_class{table_name} or ($the_data_source and ($the_data_source->initializer_should_create_column_name_for_class_properties()))) and not exists($new_property{column_name}) # They didn't supply a column_name and not $new_property{is_transient} and not $new_property{is_delegated} and not $new_property{is_calculated} and not $new_property{is_legacy_eav} ) { $new_property{column_name} = $new_property{property_name}; if ($the_data_source and $the_data_source->table_and_column_names_are_upper_case) { $new_property{column_name} = uc($new_property{column_name}); } } if ($new_property{order_by} and not $new_property{is_many}) { die "Cannot use order_by except on is_many properties!"; } if ($new_property{specify_by} and not $new_property{is_many}) { die "Cannot use specify_by except on is_many properties!"; } if ($new_property{implied_by} and $new_property{implied_by} eq $property_name) { $class->warnings_message("New data has odd self-referential 'implied_by' on $class_name $property_name!"); delete $new_property{implied_by}; } return %new_property; } sub _make_minimal_class_from_normalized_class_description { my $class = shift; my $desc = shift; my $class_name = $desc->{class_name}; unless ($class_name) { Carp::confess("No class name specified?"); } my $meta_class_name = $desc->{meta_class_name}; die unless $meta_class_name; if ($meta_class_name ne __PACKAGE__) { unless ( $meta_class_name->isa(__PACKAGE__) ) { warn "Bogus meta class $meta_class_name doesn't inherit from UR::Object::Type?" } } # only do this when the classes match # when they do not match, the super-class has already called this by delegating to the correct subclass $class_name::VERSION = 2.0; my $self = bless { id => $class_name, %$desc }, $meta_class_name; $UR::Context::all_objects_loaded->{$meta_class_name}{$class_name} = $self; my $full_name = join( '::', $class_name, '__meta__' ); Sub::Install::reinstall_sub({ into => $class_name, as => '__meta__', code => Sub::Name::subname $full_name => sub {$self}, }); return $self; } sub _initialize_accessors_and_inheritance { my $self = shift; $self->initialize_direct_accessors; my $class_name = $self->{class_name}; my @is = @{ $self->{is} }; unless (@is) { @is = ('UR::ModuleBase') } eval "\@${class_name}::ISA = (" . join(',', map { "'$_'" } @is) . ")\n"; Carp::croak("Can't initialize \@ISA for class_name '$class_name': $@\nMaybe the class_name or one of the parent classes are not valid class names") if $@; my $namespace_mro; my $namespace_name = $self->{namespace}; if ( !$bootstrapping && !$class_name->isa('UR::Namespace') && $namespace_name && $namespace_name->isa('UR::Namespace') && $namespace_name->can('get') && (my $namespace = $namespace_name->get()) ) { $namespace_mro = $namespace->method_resolution_order; } if ($^V lt v5.9.5 && $namespace_mro && $namespace_mro eq 'c3') { warn "C3 method resolution order is not supported on Perl < 5.9.5. Reverting $namespace_name namespace to DFS."; my $namespace = $namespace_name->get(); $namespace_mro = $namespace->method_resolution_order('dfs'); } if ($^V ge v5.9.5 && $namespace_mro && mro::get_mro($class_name) ne $namespace_mro) { mro::set_mro($class_name, $namespace_mro); } return $self; } our %_init_subclasses_loaded; sub subclasses_loaded { return @{ $_init_subclasses_loaded{shift->class_name}}; } our %_inform_all_parent_classes_of_newly_loaded_subclass; sub _inform_all_parent_classes_of_newly_loaded_subclass { my $self = shift; my $class_name = $self->class_name; Carp::confess("re-initializing class $class_name") if $_inform_all_parent_classes_of_newly_loaded_subclass{$class_name}; $_inform_all_parent_classes_of_newly_loaded_subclass{$class_name} = 1; no strict 'refs'; no warnings; my @parent_classes = @{ $class_name . "::ISA" }; for my $parent_class (@parent_classes) { unless ($parent_class->can("id")) { __PACKAGE__->use_module_with_namespace_constraints($parent_class); if ($@) { die "Failed to find parent_class $parent_class for $class_name!"; } } } my @i = sort $class_name->inheritance; $_init_subclasses_loaded{$class_name} ||= []; my $last_parent_class = ""; for my $parent_class (@i) { next if $parent_class eq $last_parent_class; $last_parent_class = $parent_class; $_init_subclasses_loaded{$parent_class} ||= []; push @{ $_init_subclasses_loaded{$parent_class} }, $class_name; push @{ $parent_class . "::_init_subclasses_loaded" }, $class_name; # any index on a parent class must move to the child class # if the child class were loaded before the index is made, it is pushed down at index creation time if (my $parent_index_hashrefs = $UR::Object::Index::all_by_class_name_and_property_name{$parent_class}) { #print "PUSHING INDEXES FOR $parent_class to $class_name\n"; for my $parent_property (keys %$parent_index_hashrefs) { my $parent_indexes = $parent_index_hashrefs->{$parent_property}; my $indexes = $UR::Object::Index::all_by_class_name_and_property_name{$class_name}{$parent_property} ||= []; push @$indexes, @$parent_indexes; } } } return 1; } sub _complete_class_meta_object_definitions { my $self = shift; # track related objects my @subordinate_objects; # grab some data from the object my $class_name = $self->{class_name}; my $table_name = $self->{table_name}; # decompose the embedded complex data structures into normalized objects my $inheritance = $self->{is}; my $properties = $self->{has}; my $relationships = $self->{relationships} || []; my $constraints = $self->{constraints}; my $data_source = $self->{'data_source_id'}; my $id_properties = $self->{id_by}; my %id_property_rank; for (my $i = '0 but true'; $i < @$id_properties; $i++) { $id_property_rank{$id_properties->[$i]} = $i; } # mark id/non-id properites foreach my $pinfo ( values %$properties ) { $pinfo->{'is_id'} = $id_property_rank{$pinfo->{'property_name'}}; } # handle inheritance unless ($class_name eq "UR::Object") { no strict 'refs'; # sanity check my @expected = @$inheritance; my @actual = @{ $class_name . "::ISA" }; if (@actual and "@actual" ne "@expected") { Carp::confess("for $class_name: expected '@expected' actual '@actual'\n"); } # set @{ $class_name . "::ISA" } = @$inheritance; } if (not $data_source and $class_name->can("__load__")) { # $data_source = UR::DataSource::Default->__define__; $data_source = { is => 'UR::DataSource::Default' }; } # Create inline data source if ($data_source and ref($data_source) eq 'HASH') { $self->{'__inline_data_source_data'} = $data_source; my $ds_class = $data_source->{'is'}; my $inline_ds = $ds_class->create_from_inline_class_data($self, $data_source); $self->{'data_source_id'} = $self->{'db_committed'}->{'data_source_id'} = $inline_ds->id; } if ($self->{'data_source_id'} and !defined($self->{table_name})) { my $data_source_obj = UR::DataSource->get($self->{'data_source_id'}) || eval { $self->{'data_source_id'}->get() }; if ($data_source_obj and $data_source_obj->initializer_should_create_column_name_for_class_properties() ) { $self->{table_name} = '__default__'; } } for my $parent_class_name (@$inheritance) { my $parent_class = $parent_class_name->__meta__; unless ($parent_class) { #$DB::single = 1; $parent_class = $parent_class_name->__meta__; $self->error_message("Failed to find parent class $parent_class_name\n"); return; } if (not defined $self->schema_name) { if (my $schema_name = $parent_class->schema_name) { $self->{'schema_name'} = $self->{'db_committed'}->{'schema_name'} = $schema_name; } } if (not defined $self->data_source_id) { if (my $data_source_id = $parent_class->data_source_id) { $self->{'data_source_id'} = $self->{'db_committed'}->{'data_source_id'} = $data_source_id; } } # For classes with no data source, the default for id_generator is -urinternal # For classes with a data source, autogenerate_new_object_id_for_class_name_and_rule gets called # on that data source which can use id_generator as it sees fit if (! defined($self->{'id_generator'}) and ! $self->{'data_source_id'}) { $self->{'id_generator'} = '-urinternal'; } # If a parent is declared as a singleton, we are too. # This only works for abstract singletons. if ($parent_class->is_singleton and not $self->is_singleton) { $self->is_singleton($parent_class->is_singleton); } } # when we "have" an object reference, add it to the list of old-style references # also ensure the old-style property definition is complete for my $pinfo (grep { $_->{id_by} } values %$properties) { push @$relationships, $pinfo->{property_name}, $pinfo; my $id_properties = $pinfo->{id_by}; my $r_class_name = $pinfo->{data_type}; unless($r_class_name) { die sprintf("Object accessor property definition for %s::%s has an 'id_by' but no 'data_type'", $pinfo->{'class_name'}, $pinfo->{'property_name'}); } my $r_class; my @r_id_properties; for (my $n=0; $n<@$id_properties; $n++) { my $id_property_name = $id_properties->[$n]; my $id_property_detail = $properties->{$id_property_name}; unless ($id_property_detail) { #$DB::single = 1; 1; } # No data_type specified, first try parent classes for the same property name # and use their type if (!$bootstrapping and !exists($id_property_detail->{data_type})) { if (my $inh_prop = ($self->ancestry_property_metas(property_name => $id_property_name))[0]) { $id_property_detail->{data_type} = $inh_prop->data_type; } } # Didn't find one - use the data type of the ID property(s) in the class we point to unless ($id_property_detail->{data_type}) { unless ($r_class) { # FIXME - it'd be nice if we didn't have to load the remote class here, and # instead put off loading until it's necessary $r_class ||= UR::Object::Type->get($r_class_name); unless ($r_class) { Carp::confess("Unable to load $r_class_name while defining relationship ".$pinfo->{'property_name'}. " in class $class_name"); } @r_id_properties = $r_class->id_property_names; } my ($r_property) = map { my $r_class_ancestor = UR::Object::Type->get($_); my $data = $r_class_ancestor->{has}{$r_id_properties[$n]}; ($data ? ($data) : ()); } ($r_class_name, $r_class_name->__meta__->ancestry_class_names); unless ($r_property) { #$DB::single = 1; my $property_name = $pinfo->{'property_name'}; if (@$id_properties != @r_id_properties) { Carp::croak("Can't resolve relationship for class $class_name property '$property_name': " . "id_by metadata has " . scalar(@$id_properties) . " items, but remote class " . "$r_class_name only has " . scalar(@r_id_properties) . " ID properties\n"); } else { my $r_id_property = $r_id_properties[$n] ? "'$r_id_properties[$n]'" : '(undef)'; Carp::croak("Can't resolve relationship for class $class_name property '$property_name': " . "Class $r_class_name does not have an ID property named $r_id_property, " . "which would be linked to the local property '".$id_properties->[$n]."'\n"); } } $id_property_detail->{data_type} = $r_property->{data_type}; } } next; } # make old-style (bc4nf) property objects in the default way my %property_objects; for my $pinfo (values %$properties) { my $property_name = $pinfo->{property_name}; my $property_subclass = $pinfo->{property_subclass}; # Acme::Employee::Attribute::Name is a bc6nf attribute # extends Acme::Employee::Attribute # extends UR::Object::Attribute # extends UR::Object my @words = map { ucfirst($_) } split(/_/,$property_name); #@words = $self->namespace->get_vocabulary->convert_to_title_case(@words); my $bridge_class_name = $class_name . "::Attribute::" . join('', @words); # Acme::Employee::Attribute::Name::Type is both the class definition for the bridge, # and also the attribute/property metadata for my $property_meta_class_name = $bridge_class_name . "::Type"; # define a new class for the above, inheriting from UR::Object::Property # all of the "attributes_have" get put into the class definition # call the constructor below on that new class #UR::Object::Type->__define__( ## class_name => $property_meta_class_name, # is => 'UR::Object::Property', # TODO: go through the inheritance # has => [ # @{ $class_name->__meta__->{attributes_have} } # ] #) my ($singular_name,$plural_name); unless ($pinfo->{plural_name} and $pinfo->{singular_name}) { require Lingua::EN::Inflect; if ($pinfo->{is_many}) { $plural_name = $pinfo->{plural_name} ||= $pinfo->{property_name}; $pinfo->{singular_name} = Lingua::EN::Inflect::PL_V($plural_name); } else { $singular_name = $pinfo->{singular_name} ||= $pinfo->{property_name}; $pinfo->{plural_name} = Lingua::EN::Inflect::PL($singular_name); } } my $property_object = UR::Object::Property->__define__(%$pinfo, id => $class_name . "\t" . $property_name); unless ($property_object) { $self->error_message("Error creating property $property_name for class " . $self->class_name . ": " . $class_name->error_message); for $property_object (@subordinate_objects) { $property_object->unload } $self->unload; return; } $property_objects{$property_name} = $property_object; push @subordinate_objects, $property_object; } if ($constraints) { my $property_rule_template = UR::BoolExpr::Template->resolve('UR::Object::Property','class_name','property_name'); my $n = 1; for my $unique_set (sort { $a->{sql} cmp $b->{sql} } @$constraints) { my ($name,$properties,$group,$sql); if (ref($unique_set) eq "HASH") { $name = $unique_set->{name}; $properties = $unique_set->{properties}; $sql = $unique_set->{sql}; $name ||= $sql; } else { $properties = @$unique_set; $name = '(unnamed)'; $n++; } for my $property_name (sort @$properties) { my $prop_rule = $property_rule_template->get_rule_for_values($class_name,$property_name); my $property = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $prop_rule); unless ($property) { Carp::croak("Constraint '$name' on class $class_name requires unknown property '$property_name'"); } } } } for my $obj ($self,@subordinate_objects) { #use Data::Dumper; no strict; my %db_committed = %$obj; delete @db_committed{@keys_to_delete_from_db_committed}; $obj->{'db_committed'} = \%db_committed; }; unless ($self->generate) { $self->error_message("Error generating class " . $self->class_name . " as part of creation : " . $self->error_message); for my $property_object (@subordinate_objects) { $property_object->unload } $self->unload; return; } if (my $extra = $self->{extra}) { # some class characteristics may be only present in subclasses of UR::Object # we handle these at this point, since the above is needed for bootstrapping my %still_not_found; for my $key (sort keys %$extra) { if ($self->can($key)) { $self->$key($extra->{$key}); } else { $still_not_found{$key} = $extra->{$key}; } } if (%still_not_found) { $DB::single = 1; Carp::confess("BAD CLASS DEFINITION for $class_name. Unrecognized properties: " . Data::Dumper::Dumper(%still_not_found)); } } $self->__signal_change__("load"); # The inheritance method is high overhead because of the number of times it is called. # Cache on a per-class basis. my @i = $class_name->inheritance; if (grep { $_ eq '' } @i) { print "$class_name! @{ $self->{is} }"; #$DB::single = 1; $class_name->inheritance; } Carp::confess("Odd inheritance @i for $class_name") unless $class_name->isa('UR::Object'); my $src1 = " return shift->SUPER::inheritance(\@_) if ( (ref(\$_[0])||\$_[0]) ne '$class_name'); return (" . join(", ", map { "'$_'" } (@i)) . ")"; my $src2 = qq|sub ${class_name}::inheritance { $src1 }|; eval $src2 unless $class_name eq 'UR::Object'; die $@ if $@; $self->{'_property_meta_for_name'} = \%property_objects; # return the new class object return $self; } # write the module from the existing data in the class object sub generate { my $self = shift; return 1 if $self->{'generated'}; #my %params = @_; # Doesn't seem to be used below... # The follwing code will override a lot intentionally. # Supress the warning messages. no warnings; # the class that this object represents # the class that we're going to generate # the "new class" my $class_name = $self->class_name; # this is done earlier in the class definition process in _make_minimal_class_from_normalized_class_description() my $full_name = join( '::', $class_name, '__meta__' ); Sub::Install::reinstall_sub({ into => $class_name, as => '__meta__', code => Sub::Name::subname $full_name => sub {$self}, }); my @parent_class_names = $self->parent_class_names; do { no strict 'refs'; if (@{ $class_name . '::ISA' }) { #print "already have isa for class_name $class_name: " . join(",",@{ $class_name . '::ISA' }) . "\n"; } else { no strict 'refs'; @{ $class_name . '::ISA' } = @parent_class_names; #print "setting isa for class_name $class_name: " . join(",",@{ $class_name . '::ISA' }) . "\n"; }; }; my ($props, $cols) = ([], []); # for _all_properties_columns() $self->{_all_properties_columns} = [$props, $cols]; my $id_props = []; # for _all_id_properties() $self->{_all_id_properties} = $id_props; # build the supplemental classes for my $parent_class_name (@parent_class_names) { next if $parent_class_name eq "UR::Object"; if ($parent_class_name eq $class_name) { Carp::confess("$class_name has parent class list which includes itself?: @parent_class_names\n"); } my $parent_class_meta = UR::Object::Type->get(class_name => $parent_class_name); unless ($parent_class_meta) { #$DB::single = 1; $parent_class_meta = UR::Object::Type->get(class_name => $parent_class_name); Carp::confess("Cannot generate $class_name: Failed to find class meta-data for base class $parent_class_name."); } unless ($parent_class_meta->generated()) { $parent_class_meta->generate(); } unless ($parent_class_meta->{_all_properties_columns}) { Carp::confess("No _all_properties_columns for $parent_class_name?"); } # inherit properties and columns my ($p, $c) = @{ $parent_class_meta->{_all_properties_columns} }; push @$props, @$p if $p; push @$cols, @$c if $c; my $id_p = $parent_class_meta->{_all_id_properties}; push @$id_props, @$id_p if $id_p; } # set up accessors/mutators for properties my @property_objects = UR::Object::Property->get(class_name => $self->class_name); my @id_property_objects = $self->direct_id_property_metas; my %id_property; for my $ipo (@id_property_objects) { $id_property{$ipo->property_name} = 1; } if (@id_property_objects) { $id_props = []; for my $ipo (@id_property_objects) { push @$id_props, $ipo->property_name; } } my $has_table; my @parent_classes = map { UR::Object::Type->get(class_name => $_) } @parent_class_names; for my $co ($self, @parent_classes) { if ($co->table_name) { $has_table = 1; last; } } my $data_source_obj = $self->data_source; my $columns_are_upper_case; if ($data_source_obj) { $columns_are_upper_case = $data_source_obj->table_and_column_names_are_upper_case; } my @sort_list = map { [$_->property_name, $_] } @property_objects; for my $sorted_item ( sort { $a->[0] cmp $b->[0] } @sort_list ) { my $property_object = $sorted_item->[1]; if ($property_object->column_name) { push @$props, $property_object->property_name; push @$cols, $columns_are_upper_case ? uc($property_object->column_name) : $property_object->column_name; } } # set the flag to prevent this from occurring multiple times. $self->generated(1); # read in filesystem package if there is one #$self->use_filesystem_package($class_name); # Let each class in the inheritance hierarchy do any initialization # required for this class. Note that the _init_subclass method does # not call SUPER::, but relies on this code to find its parents. This # is the only way around a sparsely-filled multiple inheritance tree. # TODO: Replace with $class_name->EVERY::LAST::_init_subclass() #unless ( # $bootstrapping # and # $UR::Object::_init_subclass->{$class_name} #) { my @inheritance = $class_name->inheritance; my %done; for my $parent (reverse @inheritance) { my $initializer = $parent->can("_init_subclass"); next unless $initializer; next if $done{$initializer}; $initializer->($class_name,$class_name) or die "Parent class $parent failed to initialize subclass " . "$class_name :" . $parent->error_message; $done{$initializer} = 1; } } unless ($class_name->isa("UR::Object")) { print Data::Dumper::Dumper('@C::ISA',\@C::ISA,'@B::ISA',\@B::ISA); } # ensure the class is generated die "Error in module for $class_name. Resulting class does not appear to be generated!" unless $self->generated; # ensure the class inherits from UR::Object die "$class_name does not inherit from UR::Object!" unless $class_name->isa("UR::Object"); return 1; } 1; ModuleWriter.pm000444023532023421 7234312121654175 20056 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Typepackage UR::Object::Type::ModuleWriter; # to help the installer package UR::Object::Type; # hold methods for the class which cover Module Read/Write. use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; our %meta_classes; our $bootstrapping = 1; our @partially_defined_classes; our $pwd_at_compile_time = cwd(); sub resolve_class_description_perl { my $self = $_[0]; no strict 'refs'; my @isa = @{ $self->class_name . "::ISA" }; use strict; unless (@isa) { #Carp::cluck("No isa for $self->{class_name}!?"); my @i = ${ $self->is }; my @parent_class_objects = map { UR::Object::Type->is_loaded(class_name => $_) } @i; die "Parent class objects not all loaded for " . $self->class_name unless (@i == @parent_class_objects); @isa = map { $_->class_name } @parent_class_objects; } unless (@isa) { #Carp::confess("FAILED TO SET ISA FOR $self->{class_name}!?"); my @i = ${ $self->is }; my @parent_class_objects = map { UR::Object::Type->is_loaded(class_name => $_) } @i; unless (@i and @i == @parent_class_objects) { #$DB::single = 1; Carp::confess("No inheritance meta-data found for ( @i / @parent_class_objects)" . $self->class_name) } @isa = map { $_->class_name } @parent_class_objects; } my $class_name = $self->class_name; my @parent_classes = $self->parent_class_metas; my $has_table = $self->has_table; # For getting default values for some of the properties my $class_meta_meta = UR::Object::Type->get(class_name => 'UR::Object::Type'); my $perl = ''; unless (@isa == 1 and $isa[0] =~ /^UR::Object|UR::Entity$/ ) { $perl .= " is => " . (@isa == 1 ? "[ '@isa' ],\n" : "[ qw/@isa/ ],\n"); } $perl .= " table_name => " . ($self->table_name ? "'" . $self->table_name . "'" : 'undef') . ",\n" if $self->data_source_id; $perl .= " is_abstract => 1,\n" if $self->is_abstract; $perl .= " er_role => '" . $self->er_role . "',\n" if ($self->er_role and ($self->er_role ne $class_meta_meta->property_meta_for_name('er_role')->default_value)); # Meta-property attributes my @property_meta_property_names; my @property_meta_property_strings; if ($self->{'attributes_have'}) { @property_meta_property_names = sort { $self->{'attributes_have'}->{$a}->{'position_in_module_header'} <=> $self->{'attributes_have'}->{$b}->{'position_in_module_header'} } keys %{$self->{'attributes_have'}}; foreach my $meta_name ( @property_meta_property_names ) { my $this_meta_struct = $self->{'attributes_have'}->{$meta_name}; # The attributes_have structure gets propogated to subclasses, but it only needs to appear # in the class definition of the most-parent class my $expected_name = $class_name . '::attributes_have'; next unless ( $this_meta_struct->{'is_specified_in_module_header'} eq $expected_name); # We want these to appear first my @this_meta_properties; push @this_meta_properties, sprintf("is => '%s'", $this_meta_struct->{'is'}) if (exists $this_meta_struct->{'is'}); push @this_meta_properties, sprintf("is_optional => %d", $this_meta_struct->{'is_optional'}) if (exists $this_meta_struct->{'is_optional'}); foreach my $key ( sort keys %$this_meta_struct ) { next if grep { $key eq $_ } qw( is is_optional is_specified_in_module_header position_in_module_header ); # skip the ones we've already done my $value = $this_meta_struct->{$key}; my $format = $self->_is_number($value) ? "%s => %s" : "%s => '%s'"; push @this_meta_properties, sprintf($format, $key, $value); } push @property_meta_property_strings, "$meta_name => { " . join(', ', @this_meta_properties) . " },"; } } if (@property_meta_property_strings) { $perl .= " attributes_have => [\n " . join("\n ", @property_meta_property_strings) . "\n ],\n"; } if (exists $self->{'first_sub_classification_method_name'}) { # This gets overridden by UR::Object::Type to cache the value it finds from parent # classes in __first_sub_classification_method_name, so we can't just get the # property through the normal channels $perl .= " first_sub_classification_method_name => '" . $self->{'first_sub_classification_method_name'} ."',\n"; } # These property names are either written in other places in this sub, or shouldn't be written out my %addl_property_names = map { $_ => 1 } $self->__meta__->all_property_type_names; my @specified = qw/is class_name table_name id_by er_role is_abstract generated data_source_id schema_name doc namespace id first_sub_classification_method_name property_metas pproperty_names id_property_metas meta_class_name id_generator valid_signals/; delete @addl_property_names{@specified}; for my $property_name (sort keys %addl_property_names) { my $property_obj = $class_meta_meta->property_meta_for_name($property_name); next if ($property_obj->is_calculated or $property_obj->is_delegated); my $property_value = $self->$property_name; my $default_value = $property_obj && $property_obj->default_value; # If the property is set on the class object # and both the value and default are numeric and numerically different, # or stringly different than the default no warnings qw( numeric uninitialized ); if ( defined $property_value and ( ($property_value + 0 eq $property_value and $default_value + 0 eq $default_value and $property_value != $default_value) or ($property_value ne $default_value) ) ) { # then it should show up in the class definition $perl .= " $property_name => '" . $self->$property_name . "',\n"; } } my %properties_by_section; my %id_property_names = map { $_ => 1 } $self->direct_id_property_names; my @properties = $self->direct_property_metas; foreach my $property_meta ( @properties ) { my $mentioned_section = $property_meta->is_specified_in_module_header; next unless $mentioned_section; # skip implied properites ($mentioned_section) = ($mentioned_section =~ m/::(\w+)$/); if (($mentioned_section and $mentioned_section eq 'id_implied') or $id_property_names{$property_meta->property_name}) { push @{$properties_by_section{'id_by'}}, $property_meta; } elsif ($mentioned_section) { push @{$properties_by_section{$mentioned_section}}, $property_meta; } else { push @{$properties_by_section{'has'}}, $property_meta; } } my %sections_seen; my $data_source_id = $self->data_source_id; my ($data_source) = ($data_source_id ? UR::DataSource->get($data_source_id) : undef); foreach my $section ( ( 'id_by', 'has', 'has_many', 'has_optional', keys(%properties_by_section) ) ) { next unless ($properties_by_section{$section}); next if ($sections_seen{$section}); $sections_seen{$section} = 1; # New properites (will have position_in_module_header == undef) should go at the end my @properties = sort { my $pos_a = defined($a->{'position_in_module_header'}) ? $a->{'position_in_module_header'} : 1000000; my $pos_b = defined($b->{'position_in_module_header'}) ? $b->{'position_in_module_header'} : 1000000; $pos_a <=> $pos_b; } @{$properties_by_section{$section}}; my $section_src = ''; my $max_name_length = 0; my $multi_line_indent = ''; foreach my $property_meta ( @properties ) { my $name = $property_meta->property_name; $max_name_length = length($name) if (length($name) > $max_name_length); } # 14 is the 8 spaces at the start of the $line, plus ' => { ' $multi_line_indent = ' ' x ($max_name_length + 14); foreach my $property_meta ( @properties ) { my $name = $property_meta->property_name; my @fields = $self->_get_display_fields_for_property( $property_meta, has_table => $has_table, section => $section, data_source => $data_source, attributes_have => \@property_meta_property_names); foreach ( @fields ) { s/\n/\n$multi_line_indent/; } my $line = " " . $name . (" " x ($max_name_length - length($name))) . " => { " . join(", ", @fields) . " },\n"; $section_src .= $line; } $perl .= " $section => [\n$section_src ],\n"; } my $unique_groups = $self->unique_property_set_hashref; if ($unique_groups and keys %$unique_groups) { $perl .= " unique_constraints => [\n"; for my $unique_group_name (keys %$unique_groups) { my $property_names = join(' ', sort { $a cmp $b } @{ $unique_groups->{$unique_group_name}}); $perl .= " { " . "properties => [qw/$property_names/], " . "sql => '" . $unique_group_name . "'" . " },\n"; } $perl .= " ],\n"; } $perl .= " schema_name => '" . $self->schema_name . "',\n" if $self->schema_name; $perl .= " data_source => '" . $self->data_source_id . "',\n" if $self->data_source_id; my $print_id_generator; if (my $id_generator = $self->id_generator) { if ($self->data_source_id and $id_generator eq '-urinternal') { $print_id_generator = 1; } elsif (! $self->data_source_id and $id_generator eq '-urinternal') { $print_id_generator = 0; } else { $print_id_generator = 1; } $perl .= " id_generator => '$id_generator',\n" if ($print_id_generator); } if (my $valid_signals = $self->valid_signals) { if (ref($valid_signals) ne 'ARRAY') { Carp::croak("The 'valid_signals' metadata for class $class_name must be an arrayref, got: ".Data::Dumper::Dumper($valid_signals)); } elsif (@$valid_signals) { $perl .= " valid_signals => ['" . join("', '", @$valid_signals) . "'],\n"; } } my $doc = $self->doc; if (defined($doc)) { $doc = Dumper($doc); $doc =~ s/\$VAR1 = //; $doc =~ s/;\s*$//; } $perl .= " doc => $doc,\n" if defined($doc); return $perl; } sub resolve_module_header_source { my $self = shift; my $class_name = $self->class_name; my $perl = "class $class_name {\n"; $perl .= $self->resolve_class_description_perl; $perl .= "};\n"; return $perl; } my $next_line_prefix = "\n"; my $deep_indent_prefix = "\n" . (" " x 55); sub _get_display_fields_for_property { my $self = shift; my $property = shift; my %params = @_; if (not $property->is_specified_in_module_header) { # we omit showing implied properties which have no additional data, # unless they have their own docs, a specified column, etc. return(); } my @fields; my %seen; my $property_name = $property->property_name; my $type = $property->data_type; if ($type) { push @fields, "is => '$type'" if $type; $seen{'is'} = 1; } if (defined($property->data_length) and length($property->data_length)) { push @fields, "len => " . $property->data_length; $seen{'data_length'} = 1; } #$line .= "references => '???', "; if ($property->is_legacy_eav) { # temp hack for entity attribute values #push @fields, "delegate => { via => 'eav_" . $property->property_name . "', to => 'value' }"; push @fields, "is_legacy_eav => 1"; $seen{'is_legacy_eav'} = 1; } elsif ($property->is_delegated) { # do nothing $seen{'is_delegated'} = 1; } elsif ($property->is_calculated) { my @calc_fields; if (my $calc_from = $property->calculate_from) { if ($calc_from and @$calc_from == 1) { push @calc_fields, "calculate_from => '" . $calc_from->[0] . "'"; } elsif ($calc_from) { push @calc_fields, "calculate_from => [ '" . join("', '", @$calc_from) . "' ]"; } } my $calc_source; foreach my $calc_type ( qw( calculate calculate_sql calculate_perl calculate_js ) ) { if ($property->$calc_type) { $calc_source = 1; push @calc_fields, "$calc_type => q(" . $property->$calc_type . ")"; } } push @calc_fields, 'is_calculated => 1' unless ($calc_source); push @fields, join(",$next_line_prefix", @calc_fields); $seen{'is_calculated'} = 1; } elsif ($params{has_table} && ! $property->is_transient) { unless ($property->column_name) { die("no column for property on class with table: " . $property->property_name . " class: " . $self->class_name . "?"); } if ( ( $params{'data_source'} and $params{'data_source'}->table_and_column_names_are_upper_case and $property->column_name ne uc($property->property_name) ) or ( $property->column_name ne $property->property_name) ) { # If the column name doesn't match the property name, write it out push @fields, "column_name => '" . $property->column_name . "'"; } $seen{'column_name'} = 1; } if (defined($property->default_value)) { my $value = $property->default_value; if (! $self->_is_number($value)) { $value = "'$value'"; } push @fields, "default_value => $value"; $seen{'default_value'} = 1; } my $implied_property = 0; if (defined($property->implied_by) and length($property->implied_by)) { push @fields, "implied_by => '" . $property->implied_by . "'"; $implied_property = 1; $seen{'implied_by'} = 1; } if (my @id_by = eval { $property->get_property_name_pairs_for_join }) { push @fields, "id_by => " . (@id_by > 1 ? '[ ' : '') . join(", ", map { "'" . $_->[0] . "'" } @id_by) . (@id_by > 1 ? ' ]' : ''); $seen{'get_property_name_pairs_for_join'} = 1; if (defined $property->id_class_by) { push @fields, sprintf("id_class_by => '%s'", $property->id_class_by); } } if ($property->via) { push @fields, "via => '" . $property->via . "'"; $seen{'via'} = 1; if ($property->to and $property->to ne $property->property_name) { push @fields, "to => '" . $property->to . "'"; $seen{'to'} = 1; } if ($property->is_mutable) { # via properties are not usually mutable push @fields, 'is_mutable => 1'; } } if ($property->reverse_as) { push @fields, "reverse_as => '" . $property->reverse_as . "'"; $seen{'reverse_as'} = 1; } if ($property->constraint_name) { push @fields, "constraint_name => '" . $property->constraint_name . "'"; $seen{'constraint_name'} = 1; } if ($property->where) { my @where_parts = (); my @where = @{ $property->where }; while (@where) { my $prop_name = shift @where; my $comparison = shift @where; if (! ref($comparison)) { # It's a strictly equals comparison. # wrap it in quotes... $comparison = "'$comparison'"; } elsif (ref($comparison) eq 'HASH') { # It's a more complicated operator my @operator_parts = (); foreach my $key ( 'operator', 'value', keys %$comparison ) { if ($comparison->{$key}) { if (ref($comparison->{$key})) { my $class_name = $property->class_name; Carp::croak("Modulewriter doesn't know how to handle property $property_name of class $class_name. Its 'where' has a non-scalar value for the '$key' key"); } push @operator_parts, "$key => '" . delete($comparison->{$key}) . "'"; } } $comparison = '{ ' . join(', ', @operator_parts) . ' } '; } else { my $class_name = $property->class_name; Carp::croak("Modulewriter doesn't know how to handle property $property_name of class $class_name. Its 'where' is not a simple scalar or hashref"); } push @where_parts, "$prop_name => $comparison"; } push @fields, 'where => [ ' . join(', ', @where_parts) . ' ]'; } if (my $valid_values_arrayref = $property->valid_values) { $seen{'valid_values'} = 1; my $value_string = Data::Dumper->new([$valid_values_arrayref])->Terse(1)->Indent(0)->Useqq(1)->Dump; push @fields, "valid_values => $value_string"; } if (my $example_values_arrayref = $property->example_values) { $seen{'example_values'} = 1; my $value_string = Data::Dumper->new([$example_values_arrayref])->Terse(1)->Indent(0)->Useqq(1)->Dump; push @fields, "example_values => $value_string"; } # All the things like is_optional, is_many, etc # show only true values, false is default # section can be things like 'has', 'has_optional' or 'has_transient_many_optional' my $section = $params{'section'}; $section =~ m/^has_(.*)/; my @sections = split('_',$1 || ''); for my $std_field_name (qw/optional abstract transient constant classwide many deprecated/) { $seen{$property_name} = 1; next if (grep { $std_field_name eq $_ } @sections); # Don't print is_optional if we're in the has_optional section my $property_name = "is_" . $std_field_name; push @fields, "$property_name => " . $property->$property_name if $property->$property_name; } foreach my $meta_property ( @{$params{'attributes_have'}} ) { my $value = $property->{$meta_property}; if (defined $value) { my $format = $self->_is_number($value) ? "%s => %s" : "%s => '%s'"; push @fields, sprintf($format, $meta_property, $value); } } my $desc = $property->doc; if ($desc && length($desc)) { $desc =~ s/([\$\@\%\\\"])/\\$1/g; $desc =~ s/\n/\\n/g; push @fields, $next_line_prefix . "doc => '$desc'"; } return @fields; } sub module_base_name { my $file_name = shift->class_name; $file_name =~ s/::/\//g; $file_name .= ".pm"; return $file_name; } sub module_path { my $self = shift; my $base_name = $self->module_base_name; my $path = $INC{$base_name}; return _abs_path_relative_to_pwd_at_compile_time($path) if $path; #warn "Module $base_name is not in \%INC!\n"; my $namespace; my $first_slash = index($base_name, '/'); if ($first_slash >= 0) { # Normal case... $namespace = substr($base_name, 0, $first_slash); $namespace .= ".pm"; } else { # This module must _be_ the namespace $namespace = $base_name; } for my $dir (map { _abs_path_relative_to_pwd_at_compile_time($_) } grep { -d $_ } @INC) { if (-e $dir . "/" . $namespace) { #warn "Found $base_name in $dir...\n"; my $try_path = $dir . '/' . $base_name; return $try_path; } } return; #Carp::confess("Failed to find a module path for class " . $self->class_name); } sub _abs_path_relative_to_pwd_at_compile_time { # not a method my $path = shift; if ($path !~ /^[\/\\]/) { $path = $pwd_at_compile_time . '/' . $path; } my $path2 = Cwd::abs_path($path); # Carp::confess("$path abs is undef?") if not defined $path2; return $path2; } sub module_directory { my $self = shift; my $base_name = $self->module_base_name; my $path = $self->module_path; return unless defined($path) and length($path); unless ($path =~ s/$base_name$//) { Carp::confess("Failed to find base name $base_name at the end of path $path!") } return $path; } sub module_source_lines { my $self = shift; my $file_name = $self->module_path; my $in = IO::File->new("<$file_name"); unless ($in) { return (undef,undef,undef); } my @module_src = <$in>; $in->close; return @module_src } sub module_source { join("",shift->module_source_lines); } sub module_header_positions { my $self = shift; my @module_src = $self->module_source_lines; my $namespace = $self->namespace; my $class_name = $self->class_name; unless ($self->namespace) { die "No namespace on $self->{class_name}?" } $namespace = 'UR' if $namespace eq $self->class_name; my $state = 'before'; my ($begin,$end,$use); for (my $n = 0; $n < @module_src; $n++) { my $line = $module_src[$n]; if ($state eq 'before') { if ($line and $line =~ /^use $namespace;/) { $use = $n; } if ( $line and ( $line =~ /^define UR::Object::Type$/ or $line =~ /^(App|UR)::Object::(Type|Class)->(define|create)\($/ or $line =~ /^class\s*$class_name\b/ ) ) { $begin = $n; $state = 'during'; } else { } } elsif ($state eq 'during') { my $old_meta_src .= $line; # FIXME this dosen't appear anywhere else?? if ($line =~ /^(\)|\}|);\s*$/) { $end = $n; $state = 'after'; } } #elsif ($state eq 'after') { # #} } # cache $self->{module_header_positions} = [$begin,$end,$use]; # return return ($begin,$end,$use); } sub module_header_source_lines { my $self = shift; my ($begin,$end,$use) = $self->module_header_positions; my @src = $self->module_source_lines; return unless $end; @src[$begin..$end]; } sub module_header_source { join("",shift->module_header_source_lines); } sub rewrite_module_header { use strict; use warnings; my $self = shift; my $package = $self->class_name; # generate new class metadata my $new_meta_src = $self->resolve_module_header_source; unless ($new_meta_src) { die "Failed to generate source code for $package!"; } my ($begin,$end,$use) = $self->module_header_positions; my $namespace = $self->namespace; $namespace = 'UR' if $namespace eq $self->class_name; unless ($namespace) { ($namespace) = ($package =~ /^(.*?)::/); } $new_meta_src = "use $namespace;\n" . $new_meta_src unless $use; # determine the path to the module # this may not exist my $module_file_path = $self->module_path; # temp safety hack if ($module_file_path =~ "/gsc/scripts/lib") { Carp::confess("attempt to write directly to the app server!"); } # determine the new source for the module my @module_src; my $old_file_data; if (-e $module_file_path) { # rewrite the existing module # find the old positions of the module header @module_src = $self->module_source_lines; # cleanup legacy cruft unless ($namespace eq 'UR') { @module_src = map { ($_ =~ m/^use UR;/?"":$_) } @module_src; } if (!grep {$_ =~ m/^use warnings;/} @module_src) { $new_meta_src = "use warnings;\n" . $new_meta_src; } if (!grep {$_ =~ m/^use strict;/} @module_src) { $new_meta_src = "use strict;\n" . $new_meta_src; } # If $begin and $end are undef, then module_header_positions() didn't find any # old code and we're inserting all brand new stuff. Look for the package declaration # and insert after that. my $len; if (defined $begin || defined $end) { $len = $end-$begin+1; } else { # is there a more fool-proof way to find it? for ($begin = 0; $begin < $#module_src; ) { last if ($module_src[$begin++] =~ m/package\s+$package;/); } $len = 0; } # replace the old lines with the new source # note that the inserted "row" is multi-line, but joins nicely below... splice(@module_src,$begin,$len,$new_meta_src); my $f = IO::File->new($module_file_path); $old_file_data = join('',$f->getlines); $f->close(); } else { # write new module source # put =cut marks around it if this is a special metadata class # the definition at the top is non-functional for bootstrapping reasons if ($meta_classes{$package}) { $new_meta_src = "=cut\n\n$new_meta_src\n\n=cut\n\n"; $self->warning_message("Meta package $package"); } @module_src = join("\n", "package " . $self->class_name . ";", "", "use strict;", "use warnings;", "", $new_meta_src, "1;", "" ); } $ENV{'HOST'} ||= ''; my $temp = "$module_file_path.$$.$ENV{HOST}"; my $temp_dir = $module_file_path; $temp_dir =~ s/\/[^\/]+$//; unless (-d $temp_dir) { print "mkdir -p $temp_dir\n"; system "mkdir -p $temp_dir"; } my $out = IO::File->new(">$temp"); unless ($out) { die "Failed to create temp file $temp!"; } for (@module_src) { $out->print($_) }; $out->close; my $rv = system qq(perl -e 'eval `cat $temp`' 2>/dev/null 1>/dev/null); $rv /= 255; if ($rv) { die "Module is not compilable with new source!"; } else { unless (rename $temp, $module_file_path) { die "Error renaming $temp to $module_file_path!"; } } UR::Context::Transaction->log_change($self, ref($self), $self->id, 'rewrite_module_header', Data::Dumper::Dumper{path => $module_file_path, data => $old_file_data}); return 1; } # TODO: move to UR::Util sub _is_number { my($self,$value) = @_; no warnings 'numeric'; my $is_number = ($value + 0) eq $value; return $is_number; } 1; =pod =head1 NAME UR::Object::Type::ModuleWriter - Helper module for UR::Object::Type responsible for writing Perl modules =head1 DESCRIPTION Subroutines within this module actually live in the UR::Object::Type namespace; this module is just a convienent place to collect them. The Module Writer is used by the class updater system (L<(UR::Namespace::Command::Update::Classes> and 'ur update classes) to add, remove and alter the Perl modules behind the classes within a Namespace. =head1 METHODS =over 4 =item resolve_module_header_source $classobj->resolve_module_header_source(); Returns a string that represents a fully-formed class definition the the given class metaobject $classobj. =item resolve_class_description_perl $classobj->resolve_class_description_perl() Used by resolve_module_header_source(). This method inspects all the applicable properties of the class metaobject and builds up a string that gets inserted between the {...} of the class definition string. =item rewrite_module_header $classobj->rewrite_module_header(); This method rewrites an existing Perl module file in place for the class metaobject, or creates a new file if one does not already exist. =item module_base_name Returns the pathname of the class's module relative to the top level directory of that class's Namespace. =item module_path Returns the fully qualified pathname of the class's module. =item module_source_lines Returns the text of the class's Perl module as a list of strings. =item module_source Returns the text of the class's Perl module as a single string. =item module_header_positions Returns a 3-element list ($begin, $end, $use) where $begin is the line number where the class header begins. $end is the line number where it ends. $use is the line number where the module declares that it use's a Namespace. =item module_header_source_lines Returns the text of the class's Perl module source where the class definition is as a list of strings. =item module_header_source Returns the text of the class's Perl module source where the class definition is as a single string. =back =head1 SEE ALSO UR::Object::Type, UR::Object::Type::Initializer =cut Initializer.pod000444023532023421 5471712121654175 20072 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type=pod =head1 NAME UR::Object::Type::Initializer - Class definition syntax =head1 SYNOPSIS UR::Object::Type->define( class_name => 'Namespace::MyClass', id_by => 'my_class_id', has => ['prop_a', 'prop_b'] ); UR::Object::Type->define( class_name => 'Namespace::MyChildClass', is => 'Namespace::MyClass', has => [ 'greeting' => { is => 'String', is_optional => 0, valid_values => ["hello","good morning","good evening"], default_value => 'hello' }, ], ); UR::Object::Type->define( class_name => 'Namespace::Helper', id_by => 'helper_id', has => [ 'my_class_id' => { is => 'String', is_optional => 0 }, 'my_class' => { is => 'Namespace::MyClass', id_by => 'my_class_id' }, 'my_class_a' => { via => 'my_class', to => 'prop_a' }, ], has_optional => [ 'other_attribute' => { is => 'Integer' } ], data_source => 'Namespace::DataSource::DB', table_name => 'HELPERS', ); UR::Object::Type->define( class_name => 'Namespace::Users', id_by => ['uid'], has => [ 'login','passwd','gid','name','home','shell'], data_source => { is => 'UR::DataSource::File', file => '/etc/passwd', column_order => ['login','passwd','uid','gid','name','home','shell', skip_first_line => 0, delimiter => ':' }, id_generator => '-uuid', ); =head1 DESCRIPTION Defining a UR class is like drawing up a blueprint of what a particular kind of object will look like. That blueprint includes the properties these objects will have, what other classes the new class inherits from, and where the source data comes from such as a database or file. =head2 The Simplest Class The simplest class definition would look like this: use UR; class Thing {}; You can create an instance of this class like this: my $thing_object = Thing->create(); Instances of this class have no properties, no backing storage location, and no inheritance. Actually, none of those statements are fully true, but we'll come back to that later... =head2 A Little Background After using UR, or another class that inherits from UR::Namespace, the above "class" syntax above can be used to define a class. The equivalent, more plain-Perl way to define a class is like this: UR::Object::Type->define( class_name => 'Thing', # the remainder of the class definition would go here, if there were any ); Classes become instances of another class called L. It has a property called class_name that contains the package that instances of these objects are blessed into. Class properties are also instances of a class called L, and those properties have properties (also UR::Object::Properties) that describe it, such as the type of data it holds, and its length. In fact, all the metadata about classes, properties, relationships, inheritance, data sources and contexts are available as instances of metadata classes. You can get information about any class currently available from the command line with the command ur show properties Thing with the caveat that "currently available" means you need to be under a namespace directory that contains the class you're describing. =head2 Making Something Useful class Vehicle { id_by => 'serial_number', has => ['color', 'weight'], has_optional => ['license_plate'], }; Here we have a basic class definition for a thing we're calling a Vehicle. It has 4 properties: serial_number, color, weight and license_plate. Three of these properties are required, meaning that when you create one, you must give a value for those properties; it is similar to a 'NOT NULL' constraint on a database column. The serial_number property is an ID property of the class, meaning that no two instances (objects) of that class can exist with the same serial_number; it is similar to having a UNIQUE index on that column (or columns) in a database. Not all vehicles have license plates, so it is optional. After that, you've effectively created five object instances. One UR::Object::Type identified by its class_name being 'Vehicle', and four UR::Object::Property objects identified by the pairs of class_name and property_name. For these four properties, class_name is always 'Vehicle' and property name is one each of serial_number, color, weight and license_plate. Objects always have one property that is called 'id'. If you have only one property in the id_by section, then the 'id' property is effectively an alias for it. If you have several id_by properties, then the 'id' property becomes an amalgamation of the directly named id properties such that no two objects of that class will have the same 'id'. If there are no id_by properties given (including MyClass above that doesn't have _any_ properties), then an implicit 'id' property will get created. Instances of that class will have an 'id' generated internally by an algorithm. Finally, if the class has more than one ID property, none of them may be called 'id', since that name will be reserved for the amalgamated-value property. You can control how IDs get autogenerated with the class' id_generator metadata. For classes that save their results in a relational database, it will get new IDs from a sequence (or some equivalent mechanism for databases that do not support sequences) based on the class' table name. If you want to force the system to use some specific sequence, for example if many classes should use the same sequence generator, then put the name of this sequence in. If the id_generator begins with a dash (-), it indicates a method should be called to generate a new ID. For example, if the name is "-uuid", then the system will call the internal method C<$class_meta->autogenerate_new_object_id_uuid>. nd will make object IDs as hex string UUIDs. The default value is '-urinternal' which makes an ID string composed of the hostname, process ID, the time the program was started and an increasing integer. If id_generator is a subroutine reference, then the sub will be called with the class metaobject and creation BoolExpr passed as parameters. You'll find that the parser for class definitions is pretty accepting about the kinds of data structures it will take. The first thing after class is used as a string to name the class. The second thing is a hashref containing key/value pairs. If the value part of the pair is a single string, as the id_by is in the Vehicle class definition, then one property is created. If the value portion is an arrayref, then each member of the array creates an additional property. =head2 Filling in the Details That same class definition can be made this way: class Vehicle { id_by => [ serial_number => { is => 'String', len => 25 }, ], has => [ color => { is => 'String' }, weight => { is => 'Number' }, license_plate => { is => 'String', len => 8, is_optional => 1 }, ], }; Here we've more explicitly defined the class' properties by giving them a type. serial_number and license_number are given a maximum length, and license_number is declared as optional. Note that having a 'has_optional' section is the same as explicitly putting 'is_optional => 1' for all those properties. The same shortcut is supported for the other boolean properties of UR::Object::Property, such as is_transient, is_mutable, is_abstract, etc. The type system is pretty lax in that there's nothing stopping you from using the method for the property to assign a string into a property declared 'Number'. Type, length, is_optional constraints are checked by calling C<__errors__()> on the object, and indirectly when data is committed back to its data source. =head2 Inheritance class Car { is => 'Vehicle', has => [ passenger_count => { is => 'Integer', default_value => 0 }, transmission_type => { is => 'String', valid_values => ['manual','automatic','cvt'] }, ], }; my $car = Car->create(color => 'blue', serial_number => 'abc123', transmission_type => 'manual'); Here we define another class called Car. It inherits from Vehicle, meaning that all the properties that apply to Vehicle instances also apply to Car instances. In addition, Car instances have two new properties. passenger_count has a default value of 0, and transmission_type is constrained to three possible values. =head2 More class properties Besides property definitions, there are other things that can be specified in a class definition. =over 4 =item is Used to name the parent class(es). Single inheritance can be specified by just listing the name of the parent class as a string. Multiple inheritance is specified by an arrayref containing the parent class names. If no 'is' is listed, then the class will inherit from 'UR::Entity' =item doc A single string to list some short, useful documentation about the class. =item data_source A string to list the data source ID. For classes with no data_source, the only objects get() can return are those that had previously been instantiated with create() or define() earlier in the program, and they do not get saved anywhere during a commit(). They do, however, exist in the object cache during the program's execution. data_source can also be a hashref to define a data source in line with the class definition. See below for more information about L. =item table_name When the class' data source is some kind of database, C Specifies the name of the table where this class' data is stored to. =item select_hint Some relational databases use hints as a way of telling the query optimizer to behave differently than usual. These hints are specified inside comments like this: /* the hint */ If the class is the primary class of a query, and it has a hint, then the hint will appear after the word 'select' in the SQL. =item join_hint If the class is part of a query where it is joined, then its hint will be added to the hints already part of the query. The primary table's hint will be first, followed by the joined class' hints in the order they are joined. All the hints are separated by a single space. =item is_abstract A flag indicating that no instances of this class may be instantiated, instead it is used as a parent of other classes. =item sub_classification_method_name Holds the name of a method that is called whenever new instances of the class are loaded from a data source. This method will be called with two arguments: the name of the class the get() was called on, and the object instance being loaded. The method should return the complete name of a subclass the object should be blessed into. =item sub_classification_property_name Works like 'sub_classification_method_name', except that the value of the property is directly used to subclass the loaded object. =back =head2 Properties properties C will print out an exhaustive list of all the properties of a Class Property. A class' properties are declared in the 'id_by' or one of the 'has' sections. Some of the more important ones: =over 4 =item class_name The name of the class this property belongs to. =item property_name The name of the property. 'property_name' and 'class_name' do not actually appear in the hashref that defines the property inside a class definition, though they are properties of UR::Object::Property instances. =item is Specifies the data type of this property. Basic types include 'String', 'Integer', 'Float'. Relationships between classes are defined by having the name of another class here. See the Relationships section of L for more information. Object properties do not normally hold Perl references to other objects, but you may use 'ARRAY' or 'HASH' here to indicate that the object will store the reference directly. Note that these properties are not usually saveable to outside data sources. =item data_type A synonym for 'is' =item len Specifies the maximum length of the data, usually in bytes. =item doc A space for useful documentation about the property =item default_value The value a property will have if it is not specified when the object is created. If used on a property that is 'via' another property (see the Indirect Properties section below), it can trigger creation of a referent object. =item is_mutable A flag indicating that this property can be changed. It is the default state of a property. Set this to 0 in the property definition if the property is not changeable after the object is created. =item is_constant A flag indicating that the value of this property may not be changed after the object is created. It is a synonym for having is mutable = 0 =item is_many Indicates that this returns a list of values. Usually used with reverse_as properties. =item is_optional Indicates that this property can hold the value undef. =back =head3 Calculated Properties =over =item is_calculated A flag indicating that the value of this property is determined from a function. =item calculate_from A listref of other property names used by the calculation =item calculate A specification for how the property is to be calculated in Perl. =over 6 =item * if the value is a coderef, it will be called when that property is accessed, and the first argument will be the object instance being acted on. =item * the value may be a string containing Perl code that is eval-ed when the accessor is called. The Perl code can refer to $self, which will hold the correct object instance during execution of that code. Any properties listed in the 'calculate_from' list will also be initialized =item * The special value 'sum' means that the values of all the properties in the calculate_from list are added together and returned =back Any property can be effectively turned into a calculated property by defining a method with the same name as the property. =back =head3 Database-backed properties =over 4 =item column_name For classes whose data is stored in a database table (meaning the class has a data_source), the column_name holds the name of the database column in its table. In the default case, the column_name is the same as the 'property_name'. =item calc_sql Specifies that this property is calculated, and its value is a string containing SQL code inserted into that property's "column" in the SELECT clause =back =head2 Relation Properties Some properties are not used to hold actual data, but instead describe some kind of relationship between two classes. For example: class Person { id_by => 'person_id', has => ['name'], }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; $person = Person->create(person_id => 1, name => 'Bob'); $thing = Thing->create(thing_id => 2, owner_id => 1); Here, Thing has a property called C. It implicitly defines a property called C. C becomes a read-only property that returns an object of type Person by using the object's value for the C property, and looking up a Person object where its ID matches. In the above case, C<$thing-Eowner> will return the same object that C<$person> contains. Indirect properties can also link classes with multiple ID properties. class City { id_by => ['name', 'state'] }; class Location { has => [ city => { is => 'String' }, state => { is => 'String' }, cityobj => { is => 'City', id_by => ['city', 'state' ] }, ], }; Note that the order the properties are linked must match in the relationship property's C and the related class's C =head2 Reverse Relationships When one class has a relation property to another, the target class can also define the converse relationship. In this case, OtherClass is the same as the first L example where the relationship from OtherClass to MyClass, but we also define the relationship in the other direction, from MyClass to OtherClass. Many Things can point back to the same Person. class Person { id_by => 'person_id', has => ['name'], has_many => [ things => { is => 'Thing', reverse_as => 'owner' }, ] }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; Note that the value for C needs to be the name of the relation property in the related class that would point back to "me". Yes, it's a bit obtuse, but it's the best we have for now. =head2 Indirect Properties When the property of a related object has meaning to another object, that relationship can be defined through an indirect property. Things already have owners, but it is also useful to know a Thing's owner's name. class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, owner_name => { via => 'owner', to => 'name', default_value => 'No one' }, ], }; $name = $thing->owner_name(); $name eq $person->name; # evaluates to true The values of indirect properties are not stored in the object. When the property's method is called, it looks up the related object through the accessor named in C, and on that result, returns whatever the method named in C returns. If one of these Thing objects is created by calling Thing->create(), and no value is specified for owner_id, owner or owner_name, then the system will find a Person object where its 'name' is 'No one' and assign the Thing's owner_id to point to that Person. If no matching Person is found, it will first create one with the name 'No one'. =head2 Alias Properties Sometimes it's useful to have a property that is an alias for another property, perhaps as a refactoring tool or to make the API clearer. The is accomilished by defining an indirect property where the 'via' is __self__. class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, titleholder => { via => '__self__', to => 'owner' }, ] }; In this case, 'titleholder' is an alias for the 'owner' property. titleholder can be called as a method any place owner is a valid method call. BoolExprs may refer to titleholder, but any such references will be rewrittn to 'owner' when they are normalized. =head2 Subclassing Members of an Abstract Class In some cases, objects may be loaded using a parent class, but all the objects are binned into some other subclass. class Widget { has => [ manufacturer => { is => 'String', valid_values => ['CoolCo','Vectornox'] }, ], is_abstract => 1, sub_classification_method_name => 'subclasser', }; sub Widget::subclasser { my($class,$pending_object) = @_; my $subclass = 'Widget::' . $pending_object->manufacturer; return $subclass; } class Widget::CoolCo { is => 'Widget', has => 'serial_number', }; class Widget::Vextornox { is => 'Widget', has => 'series_string', } my $cool_widget = Widget->create(manufacturer => 'CoolCo'); $cool_widget->isa('Widget::CoolCo'); # evaluates to true $cool_widget->serial_number(12345); # works $cool_widget->series_srting(); # dies In the class definition for the parent class, Widget, it is marked as being an abstract class, and the sub_classification_method_name specifies the name of a method to call whenever a new Widget object is created or loaded. That method is passed the pre-subclassed object and must return the fully qualified subclass name the object really belongs in. All the objects returned to the caller will be blessed into the appropriate subclass. Alternatively, a property can be designated to hold the fully qualified subclass name. class Widget { has => [ subclass_name => { is => 'String', valid_values => ['Widget::CoolCo', 'Widget::Vectornox'] }, ], is_abstract => 1, subclassify_by => 'subclass_name', } my $cool_widget = Widget->create(subclass_name => 'Widget::CoolCo'); $cool_widget = Widget::CoolCo->create(); # subclass_name is automatically "Widget::CoolCo" These subclass names will be saved to the data source if the class has a data source. Also, when objects of the base class are retrieved with get(), the results will be automatically put in the appropriate child class. =head2 Inline Data Sources If the data_source of a class definition is a hashref instead of a simple string, that defines an in-line data source. The only required item in that hashref is C, which declares what class this data source will be created from, such as "UR::DataSource::Oracle" or "UR::DataSource::File". From there, each type of data source will have its own requirements for what is allowed in an inline definition. For L-derived data sources, it accepts these keys corresponding to the properties of the same name: server, user, auth, owner For L data sources: server, file_list, column_order, sort_order, skip_first_line, delimiter, record_separator In addition, file is a synonym for server. For L data sources: column_order, sort_order, skip_first_line, delimiter, record_separator, required_for_get, constant_values, file_resolver In addition, resolve_path_with can replace C and accepts several formats: =over 4 =item subref A reference to a subroutine. In this case, C is a synonym for C. =item [ $subref, param1, param2, ..., paramn ] The subref will be called to resolve the path. Its arguments will be taken from the values in the rule from properties mentioned. =item [ $format, param1, param2, ..., paramn ] $format will be interpreted as an sprintf() format. The placeholders in the format will be filled in from the values in the rule from properties mentioned. =back Finally, C and C can be used together. In this case, resolve_path_with is a listref of property names, base_path is a string specifying the first part of the pathname. The final path is created by joining the base_path and all the property's values together with '/', as in join('/', $base_path, param1, param2, ..., paramn ) =head1 SEE ALSO L, L, L AccessorWriter000755023532023421 012121654174 17646 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/TypeProduct.pm000444023532023421 76312121654172 21745 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/AccessorWriter package UR::Object::Type::AccessorWriter::Product; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub calculate { my $self = shift; my $object = shift; my $properties = shift; my $total = 1; for my $property (@$properties) { $total *= $object->$property } return $total; }; 1; =pod =head1 NAME UR::Object::Type::AccessorWriter::Product - Implements a calculation accessor which multiplies the values of its properties =cut Sum.pm000444023532023421 73712121654174 21074 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/AccessorWriter package UR::Object::Type::AccessorWriter::Sum; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub calculate { my $self = shift; my $object = shift; my $properties = shift; my $sum = 0; for my $property (@$properties) { $sum += $object->$property } return $sum; }; 1; =pod =head1 NAME UR::Object::Type::AccessorWriter::Sum - Implements a calculation accessor which sums the values of its properties =cut View000755023532023421 012121654173 15620 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/TypeDefault000755023532023421 012121654175 17206 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/ViewText.pm000444023532023421 114212121654172 20620 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/View/Defaultpackage UR::Object::Type::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['is','direct_property_names'], }, ], ); 1; =pod =head1 NAME UR::Object::Type::View::Default::Text - View class for class metaobjects =head1 DESCRIPTION This class is used by L and L to construct the text outputted. =cut Xml.pm000444023532023421 360412121654175 20444 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/View/Defaultpackage UR::Object::Type::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Xml', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => [ 'namespace', 'table_name', 'data_source_id', 'is_abstract', 'is_final', 'is_singleton', 'is_transactional', 'schema_name', 'meta_class_name', 'first_sub_classification_method_name', 'sub_classification_method_name', { label => 'Properties', name => 'properties', subject_class_name => 'UR::Object::Property', perspective => 'default', toolkit => 'xml', aspects => [ 'is_id', 'property_name', 'column_name', 'data_type', 'is_optional' ], }, { label => 'References', name => 'all_id_by_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'default', toolkit => 'xml', aspects => [], } ], }, ], ); 1; =pod =head1 NAME UR::Object::Type::View::Default::Xml - View class for class metaobjects =head1 DESCRIPTION This class is used by L to build an html representation. =cut AvailableViews000755023532023421 012121654175 20520 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/ViewXml.pm000444023532023421 504512121654173 21755 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/View/AvailableViewspackage UR::Object::Type::View::AvailableViews::Xml; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Type::View::AvailableViews::Xml { is => 'UR::Object::View::Default::Xml', has_constant => [ perspective => { value => 'available-views' }, ], }; sub _generate_content { my $self = shift; my $subject = $self->subject; return unless $subject; my $xml_doc = XML::LibXML->createDocument(); $self->_xml_doc($xml_doc); my $target_class = $subject->class_name; my %perspectives = $self->_find_perspectives($target_class); my $perspectives = $xml_doc->createElement('perspectives'); $xml_doc->setDocumentElement($perspectives); for my $key (sort keys %perspectives) { my $perspective = $perspectives->addChild( $xml_doc->createElement('perspective') ); $perspective->addChild( $xml_doc->createAttribute('name', $key) ); for my $tool_key (sort keys %{$perspectives{$key}}) { my $toolkit = $perspective->addChild( $xml_doc->createElement('toolkit')); $toolkit->addChild( $xml_doc->createAttribute('name', $tool_key)); } } $perspectives->addChild( $xml_doc->createAttribute( 'type', $target_class )); return $xml_doc->toString(1); } sub _find_perspectives { my $self = shift; my $target_class = shift; my %perspectives; for my $class ($target_class, $target_class->inheritance) { next unless $class->isa('UR::Object'); my $namespace = $class->__meta__->namespace; my $dir = $class; $dir =~ s!::!/!g; $dir =~ s!^$namespace/!!; $dir .= '/View'; my @views = $namespace->_get_class_names_under_namespace($dir); for my $view (@views) { if(my $view_type = UR::Object::Type->get($view)) { next unless $view->isa('UR::Object::View'); my $perspective = $view_type->property_meta_for_name('perspective')->default_value; my $toolkit = $view_type->property_meta_for_name('toolkit')->default_value; unless($perspective) { $self->error_message('No perspective set on view class: ' . $view_type->class_name); next; } unless($toolkit) { $self->error_message('No toolkit set on view class: ' . $view_type->class_name); next; } $perspectives{$perspective}{$toolkit}++; } } } return %perspectives; } 1; Json.pm000444023532023421 122112121654175 22120 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Type/View/AvailableViewspackage UR::Object::Type::View::AvailableViews::Json; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use UR::Object::Type::View::AvailableViews::Xml; class UR::Object::Type::View::AvailableViews::Json { is => 'UR::Object::View::Default::Json', has_constant => [ perspective => { value => 'available-views' }, ], }; sub _jsobj { my $self = shift; my $subject = $self->subject; return unless $subject; my $target_class = $subject->class_name; my %perspectives = UR::Object::Type::View::AvailableViews::Xml::_find_perspectives($self, $target_class); return \%perspectives; } 1; View000755023532023421 012121654175 14701 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ObjectAspect.pm000444023532023421 2265312121654172 16640 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Viewpackage UR::Object::View::Aspect; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; class UR::Object::View::Aspect { id_by => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => "the id of the view object this is an aspect-of" }, number => { is => 'Integer', doc => "aspects of a view are numbered" }, ], has => [ name => { is => 'Text', is_mutable => 0, doc => 'the name of the property/method on the subject which returns the value to be viewed' }, ], has_optional => [ label => { is => 'Text', doc => 'display name for this aspect' }, position => { is => 'Scalar', doc => 'position of this aspect within the parent view (meaning is view and toolkit dependent)' }, delegate_view => { is => 'UR::Object::View', id_by => 'delegate_view_id', doc => "This aspect gets rendered via another view" }, ], }; sub create { my $class = shift; my($bx,%extra) = $class->define_boolexpr(@_); # TODO: it would be nice to have this in the class definition: # increment_for => 'parent_view' unless ($bx->value_for('number')) { if (my $parent_view_id = $bx->value_for('parent_view_id')) { my $parent_view = UR::Object::View->get($parent_view_id); my @previous_aspects = $parent_view->aspects; $bx = $bx->add_filter(number => scalar(@previous_aspects)+1); } } unless ($bx->value_for('label')) { if (my $label = $bx->value_for('name')) { $label =~ s/_/ /g; $bx = $bx->add_filter(label => $label); } } if (keys %extra) { # This is a sub-view my $delegate_subject_class_name; if (exists $extra{'subject_class_name'}) { $delegate_subject_class_name = $extra{'subject_class_name'}; } else { # FIXME This duplicates functionality below in generate_delegate_view, but generate_delegate_view() # doesn't take any args to tweak the properties of that delegated view :( # Try to figure it out based on the name of the aspect... my $parent_view; if (my $view_id = $bx->value_for('parent_view_id')) { $parent_view = UR::Object::View->get($view_id); } elsif ($bx->specifies_value_for('parent_view')) { $parent_view = $bx->value_for('parent_view'); } unless ($parent_view) { Carp::croak("Can't determine parent view from keys/values: ",join(', ', map { sprintf("%s => '%s'", $_, $extra{$_}) } keys %extra)); } my $class_meta = $parent_view->subject_class_name->__meta__; unless ($class_meta) { Carp::croak("No class metadata for class " . $parent_view->subject_class_meta . ". Can't create delegate view on aspect named " . $bx->value_for('name') ); } my $property_meta = $class_meta->property_meta_for_name($bx->value_for('name')); unless ($property_meta) { Carp::croak("No property metadata for class " . $class_meta->class_name . " property " . $bx->value_for('name') . ". Can't create delegate view on aspect named " . $bx->value_for('name')); } unless ($property_meta->data_type) { Carp::croak("Property metadata for class " . $class_meta->class_name . " property " . $property_meta->property_name . " has no data_type. Can't create delegate view on aspect named " . $bx->value_for('name')); } $delegate_subject_class_name = $property_meta->data_type; } unless ($delegate_subject_class_name) { Carp::croak("Can't determine subject_class_name for delegate view on aspect named " . $bx->value_for('name')); } my $delegate_view = $delegate_subject_class_name->create_view( perspective => $bx->value_for('perspective'), toolkit => $bx->value_for('toolkit'), %extra ); unless ($delegate_view) { Carp::croak("Can't create delegate view for aspect named " . $bx->value_for('name') . ": ".$delegate_subject_class_name->error_message); } #$bx->add_filter(delegate_view_id => $delegate_view->id); $bx = $bx->add_filter(delegate_view => $delegate_view); } my $self = $class->SUPER::create($bx); return unless $self; my $name = $self->name; unless ($name) { $self->error_message("No name specified for aspect!"); $self->delete; return; } return $self; } sub _look_for_recursion { my $self = shift; my $parent_view = $self->parent_view; my $subject = $parent_view->subject; $parent_view = $parent_view->parent_view; while($parent_view) { return 1 if ($parent_view->subject eq $subject); $parent_view = $parent_view->parent_view; } return 0; } sub generate_delegate_view { no warnings; my $self = shift; my $parent_view = $self->parent_view; my $name = $self->name; my $subject_class_name = $parent_view->subject_class_name; my $retval; my $property_meta = $subject_class_name->__meta__->property($name); my $aspect_type; if ($property_meta) { $aspect_type = $property_meta->_data_type_as_class_name; unless ($aspect_type) { Carp::confess("Undefined aspect type. Set 'is' for $name in class " . $property_meta->class_name); } unless ($aspect_type) { if (my $delegated_to_meta = $property_meta->final_property_meta) { $aspect_type = $delegated_to_meta->data_type; } } unless ($aspect_type) { Carp::confess("Property meta for class ".$property_meta->class_name." property ".$property_meta->property_name." has no data_type"); } unless ($aspect_type->can("__meta__")) { Carp::croak("$aspect_type has no meta data? cannot generate a view for $subject_class_name $name!"); } } else { unless ($subject_class_name->can($name)) { $self->error_message("No property/method $name found on $subject_class_name! Invalid aspect!"); $self->delete; Carp::croak($self->error_message); } $aspect_type = 'UR::Value::Text' } my $aspect_meta = $aspect_type->__meta__; my $delegate_view; eval { $delegate_view = $aspect_type->create_view( subject_class_name => $aspect_type, perspective => $parent_view->perspective, toolkit => $parent_view->toolkit, parent_view => $parent_view, aspects => [], ); }; unless ($delegate_view) { # try again using the "default" perspective my $err1 = $@; eval { $delegate_view = $aspect_type->create_view( subject_class_name => $aspect_type, perspective => 'default', toolkit => $parent_view->toolkit, parent_view => $parent_view, aspects => [], ); }; my $err2 = $@; unless ($delegate_view) { $self->error_message( "Error creating delegate view for $name ($aspect_type)! $err1\n" . "Also failed to fall back to the default perspective for $name ($aspect_type)! $err2" ); return; } } my @default_aspects_params = $delegate_view->_resolve_default_aspects(); # add aspects which do not "go backward" # no one wants to see an order, with a list of line items, which re-reprsent thier order on each for my $aspect_params (@default_aspects_params) { my $aspect_param_name = (ref($aspect_params) ? $aspect_params->{name} : $aspect_params); my $aspect_property_meta = $aspect_meta->property($aspect_param_name); no strict; no warnings; next if (!$aspect_property_meta or !$property_meta); if ($aspect_property_meta->reverse_as() eq $name) { } elsif ($property_meta->reverse_as eq $aspect_param_name) { } else { $delegate_view->add_aspect(ref($aspect_params) ? %$aspect_params : $aspect_params); } } $self->delegate_view($delegate_view); $retval = $delegate_view; return $retval; } 1; =pod =head1 NAME UR::Object::View::Aspect - a specification for one aspect of a view =head1 SYNOPSIS my $v = $o->create_view( perspective => 'default', toolkit => 'xml', aspects => [ 'id', 'name', 'title', { name => 'department', perspective => 'logo' }, { name => 'boss', label => 'Supervisor', aspects => [ 'name', 'title', { name => 'subordinates', perspective => 'graph by title' } ] } ] ); =cut Toolkit.pm000444023532023421 137512121654175 17027 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View =pod =head1 NAME UR::Object::View::Toolkit =head1 SYNOPSIS $v1 = $obj->create_view(toolkit => "gtk"); $v2 = $obj->create_view(toolkit => "tk"); is($v1->_toolkit_delegate, "UR::Object::View::Toolkit::Gtk"); is($v2->_toolkit_delegate, "UR::Object::View::Toolkit::Tk"); =head1 DESCRIPTION Each view delegates to one of these to interact with the toolkit environment =cut package UR::Object::View::Toolkit; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION;; require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Singleton', is_abstract => 1, has => [ toolkit_name => { is_abstract => 1, is_constant => 1 }, toolkit_module => { is_abstract => 1, is_constant => 1 }, ], ); 1; Default000755023532023421 012121654174 16264 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/ViewXsl.pm000444023532023421 1716012121654172 17550 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Xsl; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; use XML::LibXML; use XML::LibXSLT; class UR::Object::View::Default::Xsl { is => 'UR::Object::View::Default::Text', has => [ output_format => { value => 'html' }, transform => { is => 'Boolean', value => 0 }, xsl_variables => { is => 'Hash', is_optional => 1 }, rest_variable => { value => '/rest', is_deprecated => 1 }, desired_perspective => { }, xsl_path => { doc => 'web relative path starting with / where the xsl ' . 'is located when serving from a web service' }, xsl_root => { doc => 'absolute path where xsl files will be found, expected ' . 'format is $xsl_path/$output_format/$perspective/' . '$normalized_class_name.xsl' }, ] }; use Exporter 'import'; our @EXPORT_OK = qw(type_to_url url_to_type); sub _generate_content { my ($self, %params) = @_; if (!$self->desired_perspective) { $self->desired_perspective($self->perspective); } # my $subject = $self->subject; # return unless $subject; unless ($self->xsl_root && -e $self->xsl_root) { die 'xsl_root does not exist:' . $self->xsl_root; } my $xml_view = $self->_get_xml_view(%params); # my $xml_content = $xml_view->_generate_content(); my $doc = $self->_generate_xsl_doc($xml_view); if ($self->transform) { return $self->transform_xml($xml_view,$doc); #$xsl_template); } else { return $doc->toString(1); # $xsl_template; } } sub _get_xml_view { my $self = shift; my %params = @_; # get the xml for the equivalent perspective my $xml_view; eval { $xml_view = UR::Object::View->create( subject_class_name => $self->subject_class_name, perspective => $self->desired_perspective, toolkit => 'xml', %params ); }; if ($@) { # try again, for debugging, don't hate me for this $DB::single you're about to crash.. $DB::single = 1; $xml_view = UR::Object::View->create( subject_class_name => $self->subject_class_name, perspective => $self->perspective, toolkit => 'xml', %params ); } return $xml_view; } sub _generate_xsl_doc { my $self = shift; my $xml_view = shift; # subclasses typically have this as a constant value # it turns out we don't need it, since the file will be HTML.pm.xsl for xml->html conversion # my $toolkit = $self->toolkit; my $output_format = $self->output_format; my $xsl_path = $self->xsl_root; unless ($self->transform) { # when not transforming we'll return a relative path # suitable for urls $xsl_path = $self->xsl_path; } my $perspective = $self->desired_perspective; my @include_files = $self->_resolve_xsl_template_files( $xml_view, $output_format, $xsl_path, $perspective ); my $rootxsl = "/$output_format/$perspective/root.xsl"; if (!-e $xsl_path . $rootxsl) { $rootxsl = "/$output_format/default/root.xsl"; } my $commonxsl = "/$output_format/common.xsl"; if (-e $xsl_path . $commonxsl) { push(@include_files, $commonxsl); } no warnings; my $xslns = 'http://www.w3.org/1999/XSL/Transform'; my $doc = XML::LibXML::Document->new("1.0", "ISO-8859-1"); my $ss = $doc->createElementNS($xslns, 'stylesheet'); $ss->setAttribute('version', '1.0'); $doc->setDocumentElement($ss); $ss->setNamespace($xslns, 'xsl', 1); my $time = time . "000"; ## this is the wrong place for this information # since it is already part of the XML document # it shouldn't be hard coded into the transform my $display_name = $self->subject->__display_name__; my $label_name = $self->subject->__label_name__; my $set_var = sub { my $e = $doc->createElementNS($xslns, 'param'); $e->setAttribute('name', $_[0]); $e->appendChild( $doc->createTextNode( $_[1] ) ); $ss->appendChild($e) }; $set_var->('currentPerspective',$perspective); $set_var->('currentToolkit',$output_format); $set_var->('displayName',$display_name); $set_var->('labelName',$label_name); $set_var->('currentTime',$time); $set_var->('username',$ENV{'REMOTE_USER'}); if (my $id = $self->subject->id) { $set_var->('objectId', $id); } if (my $class_name = $self->subject->class) { $set_var->('objectClassName', $class_name); } if (my $vars = $self->xsl_variables) { while (my ($key,$val) = each %$vars) { $set_var->($key, $val); } } else { $set_var->('rest',$self->rest_variable); } my $rootn = $doc->createElementNS($xslns, 'include'); $rootn->setAttribute('href',"$xsl_path$rootxsl"); $ss->appendChild($rootn); for (@include_files) { my $e = $doc->createElementNS($xslns, 'include'); $e->setAttribute('href',"$xsl_path$_"); $ss->appendChild($e) } return $doc; } sub _resolve_xsl_template_files { my ($self, $xml_view, $output_format, $xsl_path, $perspective) = @_; return $xml_view->xsl_template_files( $output_format, $xsl_path, $perspective, ); } sub transform_xml { my ($self,$xml_view,$style_doc) = @_; $xml_view->subject($self->subject); my $xml_content = $xml_view->_generate_content(); # remove invalid XML entities $xml_content =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go; my $parser = XML::LibXML->new; my $xslt = XML::LibXSLT->new; my $source; if($xml_view->can('_xml_doc') and $xml_view->_xml_doc) { $source = $xml_view->_xml_doc; } else { $source = $parser->parse_string($xml_content); } # convert the xml my $stylesheet = $xslt->parse_stylesheet($style_doc); my $results = $stylesheet->transform($source); my $content = $stylesheet->output_string($results); return $content; } sub type_to_url { join( '/', map { s/(?register_function( 'urn:rest', 'typetourl', \&type_to_url ); XML::LibXSLT->register_function( 'urn:rest', 'urltotype', \&url_to_type ); 1; =pod =head1 NAME UR::Object::View::Default::Xsl - base class for views which use XSL on an XML view to generate content =head1 SYNOPSIS ##### class Acme::Product::View::OrderStatus::Html { is => 'UR::Object::View::Default::Xsl', } ##### Acme/Product/View/OrderStatus/Html.pm.xsl ##### $o = Acme::Product->get(1234); $v = $o->create_view( perspective => 'order status', toolkit => 'html', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $xml1 = $v->content; $o->qty_on_hand(200); $xml2 = $v->content; =head1 DESCRIPTION This class implements basic HTML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Xml.pm000444023532023421 2354512121654172 17546 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; use XML::Dumper; use XML::LibXML; class UR::Object::View::Default::Xml { is => 'UR::Object::View::Default::Text', has_constant => [ toolkit => { value => 'xml' }, ], has => [ _xml_doc => { is => 'XML::LibXML::Document', doc => 'The LibXML document used to create the content for this view', is_transient => 1 } ], }; sub xsl_template_files { my $self = shift; #usually this is a view without a subject attached my $output_format = shift; my $root_path = shift; my $perspective = shift || lc($self->perspective); my @xsl_names = map { $_ =~ s/::/_/g; my $pf = "/$output_format/$perspective/" . lc($_) . '.xsl'; my $df = "/$output_format/default/" . lc($_) . '.xsl'; -e $root_path . $pf ? $pf : (-e $root_path . $df ? $df : undef) } $self->all_subject_classes_ancestry; my @found_xsl_names = grep { defined } @xsl_names; return @found_xsl_names; } sub _generate_xml_doc { my $self = shift; my $subject = $self->subject(); return unless $subject; my $xml_doc = XML::LibXML->createDocument(); $self->_xml_doc($xml_doc); # the header line is the class followed by the id my $object = $xml_doc->createElement('object'); $xml_doc->setDocumentElement($object); $object->addChild( $xml_doc->createAttribute('type', $self->subject_class_name) ); $object->addChild( $xml_doc->createAttribute('id', $subject->id ) ); my $display_name = $object->addChild( $xml_doc->createElement('display_name') ); $display_name->addChild( $xml_doc->createTextNode($subject->__display_name__) ); my $label_name = $object->addChild( $xml_doc->createElement('label_name' )); $label_name->addChild( $xml_doc->createTextNode($subject->__label_name__) ); my $types = $object->addChild( $xml_doc->createElement('types') ); foreach my $c ($self->subject_class_name,$subject->__meta__->ancestry_class_names) { my $isa = $types->addChild( $xml_doc->createElement('isa') ); $isa->addChild( $xml_doc->createAttribute('type', $c) ); } unless ($self->_subject_is_used_in_an_encompassing_view()) { # the content for any given aspect is handled separately my @aspects = $self->aspects; if (@aspects) { my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->number, $_ ] } @aspects; for my $aspect (@sorted_aspects) { next if $aspect->name eq 'id'; my $aspect_node = $self->_generate_content_for_aspect($aspect); $object->addChild( $aspect_node ) if $aspect_node; #If aspect has no values, it won't be included } } } #From the XML::LibXML documentation: #If $format is 1, libxml2 will add ignorable white spaces, so the nodes content is easier to read. Existing text nodes will not be altered #If $format is 2 (or higher), libxml2 will act as $format == 1 but it add a leading and a trailing line break to each text node. return $xml_doc; } sub _generate_content { my $self = shift; my $xml_doc = $self->_generate_xml_doc; return '' unless $xml_doc; my $doc_string = $xml_doc->toString(1); # remove invalid XML entities $doc_string =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go; return $doc_string; } sub _add_perl_data_to_node { my $self = shift; my $perlref = shift; my $node = shift; my $xml_doc = $self->_xml_doc; $node ||= $xml_doc->documentElement; my $d = XML::Dumper->new; my $perldata = $d->pl2xml($perlref); my $parser = XML::LibXML->new; my $ref_xml_doc = $parser->parse_string($perldata); my $ref_root = $ref_xml_doc->documentElement; $xml_doc->adoptNode( $ref_root ); $node->addChild( $ref_root ); return 1; } sub _generate_content_for_aspect { # This does two odd things: # 1. It gets the value(s) for an aspect, then expects to just print them # unless there is a delegate view. In which case, it replaces them # with the delegate's content. # 2. In cases where more than one value is returned, it recycles the same # view and keeps the content. # # These shortcuts make it hard to abstract out logic from toolkit-specifics my $self = shift; my $aspect = shift; my $subject = $self->subject; my $xml_doc = $self->_xml_doc; my $aspect_name = $aspect->name; my $aspect_node = $xml_doc->createElement('aspect'); $aspect_node->addChild( $xml_doc->createAttribute('name', $aspect_name) ); my @value; eval { @value = $subject->$aspect_name; }; if ($@) { my ($file,$line) = ($@ =~ /at (.*?) line (\d+)$/m); my $exception = $aspect_node->addChild( $xml_doc->createElement('exception') ); $exception->addChild( $xml_doc->createAttribute('file', $file) ); $exception->addChild( $xml_doc->createAttribute('line', $line) ); $exception->addChild( $xml_doc->createCDATASection($@) ); return $aspect_node; } if (not Scalar::Util::blessed($value[0])) { # shortcut to optimize for simple scalar values without delegate views for my $value ( @value ) { my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); $value = '' if not defined $value; $value_node->addChild( $xml_doc->createTextNode($value) ); } return $aspect_node; } unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } # Delegate to a subordinate view if needed. # This means we replace the value(s) with their # subordinate widget content. my $delegate_view = $aspect->delegate_view; unless ($delegate_view) { Carp::confess("No delegate view???"); } foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); # merge the delegate view's XML into this one if ($delegate_view->can('_xml_doc') and $delegate_view->_xml_doc) { # the delegate has XML my $delegate_xml_doc = $delegate_view->_xml_doc; my $delegate_root = $delegate_xml_doc->documentElement; #cloneNode($deep = 1) $aspect_node->addChild( $delegate_root->cloneNode(1) ); } elsif (ref($value) and not $value->isa("UR::Value")) { # Note: Let UR::Values display content below # Otherwise, the delegate view has no XML object, and the value is a reference $self->_add_perl_data_to_node($value, $aspect_node); } elsif (ref($value) and $value->isa("UR::Value")) { # For a UR::Value return both a formatted value and a raw value. my $display_value_node = $aspect_node->addChild( $xml_doc->createElement('display_value') ); my $content = $delegate_view->content; $content = '' if not defined $content; $display_value_node->addChild( $xml_doc->createTextNode($content) ); my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); $content = $value->id; $value_node->addChild( $xml_doc->createTextNode($content) ); } else { # no delegate view has no XML object, and the value is a non-reference # (this is the old logic for non-delegate views when we didn't have delegate views for primitives) my $value_node = $aspect_node->addChild( $xml_doc->createElement('value') ); unless(defined $value) { $value = ''; } my $content = $delegate_view->content; $content = '' if not defined $content; $value_node->addChild( $xml_doc->createTextNode($content) ); ## old logic for delegate views with no xml doc (unused now) ## the delegate view may not be XML at all--wrap it in our aspect tag so that it parses ## (assuming that whatever delegate was selected properly escapes anything that needs escaping) # my $delegate_text = $delegate_view->content() ? $delegate_view->content() : ''; # my $aspect_text = "\n$delegate_text\n"; # my $parser = XML::LibXML->new; # my $delegate_xml_doc = $parser->parse_string($aspect_text); # $aspect_node = $delegate_xml_doc->documentElement; # $xml_doc->adoptNode( $aspect_node ); } } return $aspect_node; } # Do not return any aspects by default if we're embedded in another view # The creator of the view will have to specify them manually sub _resolve_default_aspects { my $self = shift; unless ($self->parent_view) { return $self->SUPER::_resolve_default_aspects; } return; } 1; =pod =head1 NAME UR::Object::View::Default::Xml - represent object state in XML format =head1 SYNOPSIS $o = Acme::Product->get(1234); $v = $o->create_view( toolkit => 'xml', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $xml1 = $v->content; $o->qty_on_hand(200); $xml2 = $v->content; =head1 DESCRIPTION This class implements basic XML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Gtk2.pm000444023532023421 367712121654173 17602 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Gtk2; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::View::Default::Gtk2 { is => 'UR::Object::View', has_constant => [ perspective => { value => 'default'}, toolkit => { value => 'gtk2'}, ], }; sub _create_widget { my $self = shift; my $label = Gtk2::Label->new(""); return $label; } sub _update_view_from_subject { my $self = shift; my $subject = $self->subject(); my @aspects = $self->aspects; my $widget = $self->widget(); my $text = $self->subject_class_name; $text .= " with id " . $subject->id if $subject; # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->position, $_ ] } @aspects; for my $aspect (@sorted_aspects) { my $label = $aspect->label; $text .= "\n" . $label . ": "; if ($subject) { my @value = $subject->$label; $text .= join(", ", @value); } else { $text .= "-"; } } } $widget->set_text($text); return 1; } sub _update_subject_from_view { Carp::confess("This widget shouldn't be able to write to the object, it's a label? How did I get called?"); } sub _add_aspect { shift->_update_view_from_subject; } sub _remove_aspect { shift->_update_view_from_subject; } 1; =pod =head1 NAME UR::Object::View::Default::Gtk2 - Gtk2 adaptor for object views =head1 DESCRIPTION This class provides code that implements a basic Gtk2 renderer for UR objects. =head1 SEE ALSO UR::Object::View, UR::Object =cut Gtk.pm000444023532023421 402612121654173 17505 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Gtk; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View', has_constant => [ perspective => { value => 'default' }, toolkit => { value => 'gtk' }, ] ); sub _create_widget { my $self = shift; my $label = Gtk::Label->new(""); return $label; } sub _update_view_from_subject { my $self = shift; my @changes = @_; # this is not currently resolved and passed-in my $subject = $self->subject(); my @aspects = $self->aspects; my $widget = $self->widget(); my $text = $self->subject_class_name; $text .= " with id " . $subject->id if $subject; # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->position, $_ ] } @aspects; for my $aspect (@sorted_aspects) { my $label = $aspect->label; $text .= "\n" . $label . ": "; if ($subject) { my @value = $subject->$label; $text .= join(", ", @value); } else { $text .= "-"; } } } $widget->set_text($text); return 1; } sub _update_subject_from_view { Carp::confess("This widget shouldn't be able to write to the object, it's a label? How did I get called?"); } sub _add_aspect { shift->_update_view_from_subject; } sub _remove_aspect { shift->_update_view_from_subject; } 1; =pod =head1 NAME UR::Object::View::Default::Gtk - Gtk adaptor for object views =head1 DESCRIPTION This class provides code that implements a basic Gtk renderer for UR objects. =head1 SEE ALSO UR::Object::View, UR::Object =cut Html.pm000444023532023421 272612121654174 17672 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Html; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; class UR::Object::View::Default::Html { is => 'UR::Object::View::Default::Xsl', has => { output_format => { value => 'html' }, transform => { value => 1 }, toolkit => { value => 'html' }, } }; 1; =pod =head1 NAME UR::Object::View::Default::Html - represent object state in HTML format =head1 SYNOPSIS ##### package Acme::Product::View::OrderStatus::Html; class Acme::Product::View::OrderStatus::Html { is => 'UR::Object::View::Default::Html', }; sub _generate_content { my $self = shift; my $subject = $self->subject; my $html = ... .... return $html; } ##### $o = Acme::Product->get(1234); $v = $o->create_view( perspective => 'order status', toolkit => 'html', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $html1 = $v->content; $o->qty_on_hand(200); $html2 = $v->content; =head1 DESCRIPTION This class implements basic HTML views of objects. It has standard behavior for all text views. =head1 SEE ALSO UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Text.pm000444023532023421 1737212121654174 17735 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::View::Default::Text { is => 'UR::Object::View', has_constant => [ perspective => { value => 'default' }, toolkit => { value => 'text' }, ], has => [ indent_text => { is => 'Text', default_value => ' ', doc => 'indent child views with this text' }, ], }; # general view API sub _create_widget { # The "widget" for a text view is a pair of items: # - a scalar reference to hold the content # - an I/O handle to which it will display (the "window" it lives in) # Note that the former could be something tied to an object, # a file, or other external storage, though it is # simple by default. The later might also be tied. # The later is STDOUT unless overridden/changed. my $self = shift; my $scalar_ref = ''; my $fh = 'STDOUT'; return [\$scalar_ref,$fh]; } sub show { # Showing a text view typically prints to STDOUT my $self = shift; my $widget = $self->widget(); my ($content_ref,$output_stream) = @$widget; $output_stream->print($$content_ref,"\n"); } sub _update_subject_from_view { Carp::confess('currently text views are read-only!'); } sub _update_view_from_subject { my $self = shift; my $content = $self->_generate_content(@_); my $widget = $self->widget(); my ($content_ref,$fh) = @$widget; $$content_ref = $content; return 1; } # text view API sub content { # retuns the current value of the scalar ref containing the text content. my $self = shift; my $widget = $self->widget(); if (@_) { die "the widget reference for a view isn't changeable. change its content.."; } my ($content_ref,$output_stream) = @$widget; return $$content_ref; } sub output_stream { # retuns the current value of the handle to which we render. my $self = shift; my $widget = $self->widget(); if (@_) { return $widget->[1] = shift; } my ($content_ref,$output_stream) = @$widget; return $output_stream; } sub _generate_content { my $self = shift; # the header line is the class followed by the id my $text = $self->subject_class_name; $text =~ s/::/ /g; my $subject = $self->subject(); if ($subject) { my $subject_id_txt = $subject->id; $subject_id_txt = "'$subject_id_txt'" if $subject_id_txt =~ /\s/; $text .= " $subject_id_txt"; } # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; # the content for any given aspect is handled separately my @aspects = $self->aspects; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->number, $_ ] } @aspects; for my $aspect (@sorted_aspects) { next if $aspect->name eq 'id'; my $aspect_text = $self->_generate_content_for_aspect($aspect); $text .= $aspect_text; } } return $text; } sub _generate_content_for_aspect { # This does two odd things: # 1. It gets the value(s) for an aspect, then expects to just print them # unless there is a delegate view. In which case, it replaces them # with the delegate's content. # 2. In cases where more than one value is returned, it recycles the same # view and keeps the content. # # These shortcuts make it hard to abstract out logic from toolkit-specifics my $self = shift; my $aspect = shift; my $subject = $self->subject; my $indent_text = $self->indent_text; my $aspect_text = $indent_text . $aspect->label . ": "; if (!$subject) { $aspect_text .= "-\n"; return $aspect_text; } my $aspect_name = $aspect->name; my @value; eval { @value = $subject->$aspect_name; }; if (@value == 0) { $aspect_text .= "-\n"; return $aspect_text; } if (@value == 1 and ref($value[0]) eq 'ARRAY') { @value = @{$value[0]}; } unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } # Delegate to a subordinate view if needed. # This means we replace the value(s) with their # subordinate widget content. if (my $delegate_view = $aspect->delegate_view) { # TODO: it is bad to recycle a view here?? # Switch to a set view, which is the standard lister. foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); $value = $delegate_view->content(); } } if (@value == 1 and defined($value[0]) and index($value[0],"\n") == -1) { # one item, one row in the value or sub-view of the item: $aspect_text .= $value[0] . "\n"; } else { my $aspect_indent; if (@value == 1) { # one level of indent for this sub-view's sub-aspects # zero added indent for the identity line b/c it's next-to the field label # aspect1: class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_indent = $indent_text; } else { # two levels of indent for this sub-view's sub-aspects # just one level for each identity # aspect1: ... # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_text .= "\n"; $aspect_indent = $indent_text . $indent_text; } for my $value (@value) { my $value_indented = ''; if (defined $value) { my @rows = split(/\n/,$value); $value_indented = join("\n", map { $aspect_indent . $_ } @rows); chomp $value_indented; } $aspect_text .= $value_indented . "\n"; } } return $aspect_text; } 1; =pod =head1 NAME UR::Object::View::Default::Text - object views in text format =head1 SYNOPSIS $o = Acme::Product->get(1234); # generates a UR::Object::View::Default::Text object: $v = $o->create_view( toolkit => 'text', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $txt1 = $v->content; $o->qty_on_hand(200); $txt2 = $v->content; =head1 DESCRIPTION This class implements basic text views of objects. It is used for command-line tools, and is the base class for other specific text formats like XML, HTML, JSON, etc. =head1 WRITING A SUBCLASS # In Acme/Product/View/OutstandingOrders/Text.pm package Acme::Product::View::OutstandingOrders::Text; use UR; class Acme::Product::View::OutstandingOrders::Text { is => 'UR::Object::View::Default::Text' }; sub _initial_aspects { return ( 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ); } $v = $o->create_view(perspective => 'outstanding orders', toolkit => 'text'); print $v->content; =head1 SEE ALSO UR::Object::View, UR::Object::View::Toolkit::Text, UR::Object =cut Json.pm000444023532023421 645312121654174 17700 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Defaultpackage UR::Object::View::Default::Json; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use JSON; class UR::Object::View::Default::Json { is => 'UR::Object::View::Default::Text', has_constant => [ toolkit => { value => 'json' }, ], has_optional => [ encode_options => { is => 'ARRAY', default_value => ['ascii', 'pretty', 'allow_nonref', 'canonical'], doc => 'Options to enable on the JSON object; see the documentation for the JSON Perl module' }, ], }; my $json; sub _json { my ($self) = @_; return $json if defined $json; $json = JSON->new; foreach my $opt ( @{ $self->encode_options } ) { eval { $json = $json->$opt; }; if ($@) { Carp::croak("Can't initialize JSON object for encoding. Calling method $opt from encode_options died: $@"); } if (!$json) { Carp::croak("Can't initialize JSON object for encoding. Calling method $opt from encode_options returned false"); } } return $json; } sub _generate_content { my $self = shift; return $self->_json->encode($self->_jsobj); } sub _jsobj { my $self = shift; my $subject = $self->subject(); return '' unless $subject; my %jsobj = (); for my $aspect ($self->aspects) { my $val = $self->_generate_content_for_aspect($aspect); $jsobj{$aspect->name} = $val if defined $val; } return \%jsobj; } sub _generate_content_for_aspect { my $self = shift; my $aspect = shift; my $subject = $self->subject; my $aspect_name = $aspect->name; my $aspect_meta = $self->subject_class_name->__meta__->property($aspect_name); #warn $aspect_name if ref($subject) =~ /Set/; my @value; eval { @value = $subject->$aspect_name; }; if ($@) { warn $@; return; } # Always look for a delegate view. # This means we replace the value(s) with their # subordinate widget content. unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } my $ref = []; if (my $delegate_view = $aspect->delegate_view) { foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); if ($delegate_view->can('_jsobj')) { push @$ref, $delegate_view->_jsobj; } else { my $delegate_text = $delegate_view->content(); push @$ref, $delegate_text; } } } else { for my $value (@value) { if (ref($value)) { push @$ref, 'ref'; #TODO(ec) make this render references } else { push @$ref, $value; } } } if ($aspect_meta && $aspect_meta->is_many) { return $ref; } else { return shift @$ref; } } # Do not return any aspects by default if we're embedded in another view # The creator of the view will have to specify them manually sub _resolve_default_aspects { my $self = shift; unless ($self->parent_view) { return $self->SUPER::_resolve_default_aspects; } return ('id'); } 1; Toolkit000755023532023421 012121654172 16323 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/ViewText.pm000444023532023421 243012121654172 17741 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Toolkitpackage UR::Object::View::Toolkit::Text; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Toolkit', has => [ toolkit_name => { is_constant => 1, value => "text" }, toolkit_module => { is_constant => 1, value => "(none)" }, # is this used anywhere? ] ); sub show_view { my $class = shift; my $view = shift; my $widget = $view->widget; return $$widget; } # This doesn't really apply for text?! sub hide_view { return undef; my $class = shift; my $view = shift; my $widget = $view->widget; print "DEL: $widget\n"; return 1; } # This doesn't really apply for text?! sub create_window_for_view { return undef; my $class = shift; my $view = shift; my $widget = $view->widget; print "WIN: $widget\n"; return 1; } # This doesn't really apply for text?! sub delete_window_around_view { return undef; my $class = shift; my $widget = shift; print "DEL: $widget\n"; return 1; } 1; =pod =head1 NAME UR::Object::View::Toolkit::Text - Declaration of Text as a View toolkit type =head1 SYNOPSIS Methods called by UR::Object::View to get toolkit specific support for common tasks. =cut Lister000755023532023421 012121654172 16140 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/ViewText.pm000444023532023421 731712121654172 17567 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Listerpackage UR::Object::View::Lister::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', ); sub _update_view_from_subject { my $self = shift; my @changes = @_; # this is not currently resolved and passed-in my $subject = $self->subject(); my $subject_class_meta = $subject->__meta__; my @aspects = $self->aspects; my %data_for_this_object; my(%aspects_requiring_joins_by_name,%aspects_requiring_joins_by_via); my %column_for_label; for (my $i = 0; $i < @aspects; $i++) { my $aspect = $aspects[$i]; my $label = $aspect->label; my $aspect_name = $aspect->name; $column_for_label{$label} = $i; my $property_meta = $subject_class_meta->property_meta_for_name($aspect_name); if (my $via = $property_meta->via and $property_meta->is_many) { $aspects_requiring_joins_by_name{$aspect_name} = $via; $aspects_requiring_joins_by_via{$via} ||= []; push @{$aspects_requiring_joins_by_via{$via}}, $aspect_name; } if ($subject) { my @value = $subject->$aspect_name; if (@value == 1 and ref($value[0]) eq 'ARRAY') { @value = @{$value[0]}; } # Delegate to a subordinate view if need be if ($aspect->delegate_view_id) { my $delegate_view = $aspect->delegate_view; foreach my $value ( @value ) { $delegate_view->subject($value); $delegate_view->_update_view_from_subject(); $value = $delegate_view->content(); } } if (@value == 1) { $data_for_this_object{$label} = $value[0]; } else { $data_for_this_object{$label} = \@value; } } } if (keys(%aspects_requiring_joins_by_via) > 1) { $self->error_message("Viewing delegated properties via more than one property is not supported"); return; } # fill in the first row of data my @retval = (); foreach my $aspect ( @aspects ) { my $label = $aspect->label; my $col = $column_for_label{$label}; if (ref($data_for_this_object{$label})) { # it's a multi-value $retval[0]->[$col] = shift @{$data_for_this_object{$label}}; } else { $retval[0]->[$col] = $data_for_this_object{$label}; } } foreach my $via ( keys %aspects_requiring_joins_by_via ) { while(1) { my @this_row; foreach my $prop ( @{$aspects_requiring_joins_by_via{$via}} ) { my $data; if (ref($data_for_this_object{$prop}) eq 'ARRAY') { $data = shift @{$data_for_this_object{$prop}}; next unless $data; } else { $data = $data_for_this_object{$prop}; $data_for_this_object{$prop} = []; } $this_row[$column_for_label{$prop}] = $data; } last unless @this_row; push @retval, \@this_row; } } foreach my $row ( @retval ) { no warnings 'uninitialized'; $row = join("\t",@$row); } my $text = join("\n", @retval); # The text widget won't print anything until show(), # so store the data in the buffer for now my $widget = $self->widget; ${$widget->[0]} = $text; # Update the contents return 1; } sub _update_subject_from_view { 1; } sub _add_aspect { 1; } sub _remove_aspect { 1; } 1; Static000755023532023421 012121654175 16130 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/ViewHtml.pm000444023532023421 332412121654175 17531 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/View/Staticpackage UR::Object::View::Static::Html; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::View::Static::Html { is => 'UR::Object::View', has => { output_format => { value => 'html' }, html_root => { doc => 'path to plain-old html files' } }, has_constant => [ perspective => { value => 'static' }, toolkit => { value => 'html' }, ], }; sub content { my ($self) = @_; my $filename = class_to_filename($self->subject->class); my $perspective = $self->perspective() || die "Error: I have no perspective"; my $pathname = join('/', $self->html_root(), $perspective, $filename); open(my $fh, $pathname); if (!$fh) { die "Could not open the static html file: $pathname"; } my $c = do { undef $/; <$fh>; }; close($pathname); return $c; } sub class_to_filename { my ($class) = @_; $class = lc($class); $class =~ s/::/_/g; $class .= '.html'; return $class; } 1; =pod =head1 NAME UR::Object::View::Static::Html - represent object state in HTML format =head1 SYNOPSIS package Genome::Sample::Set::View::Detail::Html; class Genome::Sample::Set::View::Detail::Html { is => 'UR::Object::View::Static::Html', has_constant => [ toolkit => { value => 'html' }, perspective => { value => 'detail' } ] }; =head1 DESCRIPTION The current default HTML class creates HTML by getting XML and applying XSL. This class, on the other hand, displays some static html =head1 SEE ALSO UR::Object::View::Default::Html, UR::Object::View::Default::Text, UR::Object::View, UR::Object::View::Toolkit::XML, UR::Object::View::Toolkit::Text, UR::Object =cut Set000755023532023421 012121654172 14517 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ObjectView000755023532023421 012121654172 15431 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/SetDefault000755023532023421 012121654175 17020 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Set/ViewJson.pm000444023532023421 60012121654172 20375 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Json; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Set::View::Default::Json { is => 'UR::Object::View::Default::Json', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Html.pm000444023532023421 60012121654172 20370 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Html; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Set::View::Default::Html { is => 'UR::Object::View::Default::Html', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Text.pm000444023532023421 60012121654173 20411 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Set::View::Default::Text { is => 'UR::Object::View::Default::Text', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Xml.pm000444023532023421 57512121654175 20242 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Set/View/Default package UR::Object::Set::View::Default::Xml; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Set::View::Default::Xml { is => 'UR::Object::View::Default::Xml', has_constant => [ default_aspects => { value => [ 'rule_display', 'members' ] } ] }; 1; Property000755023532023421 012121654172 15610 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ObjectView000755023532023421 012121654175 16525 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/PropertyReferenceDescription000755023532023421 012121654172 22624 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/ViewText.pm000444023532023421 332412121654172 24245 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/View/ReferenceDescriptionpackage UR::Object::Property::View::ReferenceDescription::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', doc => "View used by 'ur show properties' for each object-accessor property", ); sub _update_view_from_subject { my $self = shift; my $property_meta = $self->subject; return unless ($property_meta); my $r_class_name = $property_meta->data_type; my @relation_detail; my @pairs = eval { $property_meta->get_property_name_pairs_for_join() }; my $text; if (@pairs) { foreach my $pair ( @pairs ) { my($property_name, $r_property_name) = @$pair; push @relation_detail, "$r_property_name => \$self->$property_name"; } my $padding = length($r_class_name) + 34; my $relation_detail = join(",\n" . " "x$padding, @relation_detail); $text = sprintf(" %22s => %s->get(%s)\n", $property_meta->property_name, $r_class_name, $relation_detail); } else { $text = sprintf(" %22s => %s->get(id => \$self->%s)\n", $property_meta->property_name, $r_class_name, $property_meta->property_name); } my $widget = $self->widget(); my $buffer_ref = $widget->[0]; $$buffer_ref = $text; return 1; } 1; =pod =head1 NAME UR::Object::Property::View::DescriptionLineItem::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Show::Properties when displaying information about a property =cut DescriptionLineItem000755023532023421 012121654174 22436 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/ViewText.pm000444023532023421 424112121654174 24056 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/View/DescriptionLineItempackage UR::Object::Property::View::DescriptionLineItem::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', doc => "View used by 'ur show properties' for each property line item", ); sub _update_view_from_subject { my $self = shift; my $property_meta = $self->subject; return unless ($property_meta); my $nullable = $property_meta->is_optional ? "NULLABLE" : ""; my $column_name = $property_meta->column_name; unless ($column_name) { if ($property_meta->via) { $column_name = $property_meta->via . '->' . $property_meta->to; } elsif ($property_meta->is_classwide) { $column_name = '(classwide)'; } elsif ($property_meta->is_delegated) { # delegated, but not via. Must be an object accessor $column_name = '' } elsif ($property_meta->is_calculated) { my $calc_from = $property_meta->calculate_from; if ($calc_from and @$calc_from) { $column_name = '(calculated from ' . join(',',@$calc_from). ')'; } else { $column_name = '(calculated)'; } } else { $column_name = '(no column)'; } } my $data_type_string; if (defined $property_meta->data_type) { my $len = $property_meta->data_length; $data_type_string = $property_meta->data_type . ( $len ? "(".$len.")" : ""); } else { $data_type_string = '(no type)'; } my $text = sprintf(" %2s %30s %-40s %25s $nullable", $property_meta->is_id ? "ID" : " ", $property_meta->property_name, $column_name, $data_type_string, ); my $widget = $self->widget(); my $buffer_ref = $widget->[0]; $$buffer_ref = $text; return 1; } 1; =pod =head1 NAME UR::Object::Property::View::DescriptionLineItem::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Show::Properties when displaying information about a property =cut Default000755023532023421 012121654175 20111 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/ViewText.pm000444023532023421 116512121654175 21533 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Property/View/Defaultpackage UR::Object::Property::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['class_name', 'property_name','data_type', 'is_optional'], }, ], ); 1; =pod =head1 NAME UR::Object::Property::View::Default::Text - View class for UR::Object::Property =head1 DESCRIPTION Used by UR::Namespace::Command::Info when displaying information about a property =cut Command000755023532023421 012121654174 15344 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ObjectList.pod000444023532023421 331512121654173 17121 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Command=pod =head1 NAME UR::Object::Command::List - Fetches and lists objects in different styles. =head1 SYNOPSIS package MyLister; use strict; use warnings; use above "UR"; class MyLister { is => 'UR::Object::Command::List', has => [ # add/modify properties ], }; 1; =head1 Provided by the Developer =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 show (optional) Add defaults to the show property: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ show => { default_value => 'name,age', }, ], }; =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =head1 List Styles text, csv, html, xml, pretty (inprogress) =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/perl_modules/trunk/UR/Object/Command/List.pm $ #$Id: List.pm 50329 2009-08-25 20:10:00Z abrummet $ FetchAndDo.pm000444023532023421 2144512121654174 20024 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Commandpackage UR::Object::Command::FetchAndDo; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use Data::Dumper; class UR::Object::Command::FetchAndDo { is => 'Command', is_abstract => 1, has => [ subject_class => { is => 'UR::Object::Type', id_by => 'subject_class_name', }, filter => { is => 'Text', is_optional => 1, doc => 'Filter results based on the parameters. See below for how to.' }, _fields => { is_many => 1, is_optional => 1, doc => 'Methods which the caller intends to use on the fetched objects. May lead to pre-fetching the data.' }, ], }; ######################################################################## sub help_detail { my $class = shift; return $class->_filter_doc; } sub _filter_doc { my $class = shift; my $doc = < and & Operators: ---------- = (exactly equal to) ~ (like the value) : (in the list of several values, slash "/" separated) (or between two values, dash "-" separated) > (greater than) >= (greater than or equal to) < (less than) <= (less than or equal to) Examples: --------- EOS if (my $help_synopsis = $class->help_synopsis) { $doc .= " $help_synopsis\n"; } else { $doc .= <200,job~%manager lister-command --filter cost:20000-90000 lister-command --filter answer:yes/maybe EOS } $doc .= <create; if ( not $self->subject_class_name and my $subject_class_name = $self->_resolved_params_from_get_options->{subject_class_name} ) { $self = $class->create(subject_class_name => $subject_class_name); } if ( $self->subject_class_name ) { if ( my @properties = $self->_subject_class_filterable_properties ) { my $longest_name = 0; foreach my $property ( @properties ) { my $name_len = length($property->property_name); $longest_name = $name_len if ($name_len > $longest_name); } for my $property ( @properties ) { my $property_doc = $property->doc; unless ($property_doc) { eval { foreach my $ancestor_class_meta ( $property->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property->property_name); if ($ancestor_property_meta and $ancestor_property_meta->doc) { $property_doc = $ancestor_property_meta->doc; last; } } }; } $property_doc ||= ' (undocumented)'; $property_doc =~ s/\n//gs; # Get rid of embeded newlines my $data_type = $property->data_type || ''; $data_type = ucfirst(lc $data_type); $doc .= sprintf(" %${longest_name}s ($data_type): $property_doc\n", $property->property_name); } } else { $doc .= sprintf(" %s\n", $self->error_message); } } else { $doc .= " Can't determine the list of filterable properties without a subject_class_name"; } return $doc; } ######################################################################## sub execute { my $self = shift; $self->_validate_subject_class or return; my $iterator = $self->_fetch or return; return $self->_do($iterator); } sub _validate_subject_class { my $self = shift; my $subject_class_name = $self->subject_class_name; $self->error_message("No subject_class_name indicated.") and return unless $subject_class_name; $self->error_message( sprintf( 'This command is not designed to work on a base UR class (%s).', $subject_class_name, ) ) and return if $subject_class_name =~ /^UR::/; UR::Object::Type->use_module_with_namespace_constraints($subject_class_name); my $subject_class = $self->subject_class; $self->error_message( sprintf( 'Can\'t get class meta object for class (%s). Is this class a properly declared UR::Object?', $subject_class_name, ) ) and return unless $subject_class; $self->error_message( sprintf( 'Can\'t find method (all_property_metas) in %s. Is this a properly declared UR::Object class?', $subject_class_name, ) ) and return unless $subject_class->can('all_property_metas'); return 1; } sub _subject_class_filterable_properties { my $self = shift; $self->_validate_subject_class or return; my %props = map { $_->property_name => $_ } $self->subject_class->property_metas; return map { $_->[1] } # These maps are to get around a bug in perl 5.8 sort { $a->[0] cmp $b->[0] } # sort involving methdo calls inside the sort sub that map { [ $_->property_name, $_ ] } # might do sorts of their own grep { substr($_->property_name, 0, 1) ne '_' } # Skip 'private' properties starting with '_' grep { ! $_->data_type or index($_->data_type, '::') == -1 } # Can't filter object-type properties from a lister, right? values %props; } sub _hint_string { return; } sub _base_filter { return; } sub _complete_filter { my $self = shift; return join(',', grep { defined $_ } $self->_base_filter,$self->filter); } sub _fetch { my $self = shift; my ($bool_expr, %extra) = UR::BoolExpr->resolve_for_string( $self->subject_class_name, $self->_complete_filter, $self->_hint_string ); $self->error_message( sprintf('Unrecognized field(s): %s', join(', ', keys %extra)) ) and return if %extra; if (my $i = $self->subject_class_name->create_iterator($bool_expr)) { return $i; } else { $self->error_message($self->subject_class_name->error_message); return; } } sub _do { shift->error_message("Abstract class. Please implement a '_do' method in your subclass."); return; } 1; =pod =head1 NAME UR::Object::Command::FetchAndDo - Base class for fetching objects and then performing a function on/with them. =head1 SYNOPSIS package MyFecthAndDo; use strict; use warnings; use above "UR"; class MyFecthAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ # other properties... ], }; sub _do { # required my ($self, $iterator) = @_; while (my $obj = $iterator->next) { ... } return 1; } 1; =head1 Provided by the Developer =head2 _do (required) Implement this method to 'do' unto the iterator. Return true for success, false for failure. sub _do { my ($self, $iterator) = @_; while (my $obj = $iterator->next) { ... } return 1; } =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/distro/ur-bundle/trunk/lib/UR/Object/Command/FetchAndDo.pm $ #$Id: FetchAndDo.pm 47408 2009-06-01 03:53:45Z ssmith $# List.pm000444023532023421 3726312121654174 17005 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Commandpackage UR::Object::Command::List; use strict; use warnings; use IO::File; use Data::Dumper; require Term::ANSIColor; use UR; use UR::Object::Command::List::Style; use List::Util qw(reduce); use Command::V2; our $VERSION = "0.41"; # UR $VERSION; class UR::Object::Command::List { is => 'Command::V2', has_input => [ subject_class_name => { is => 'ClassName', doc => 'the type of object to list', }, filter => { is => 'Text', is_optional => 1, doc => 'Filter results based on the parameters. See below for details.', shell_args_position => 1, }, show => { is => 'Text', is_optional => 1, doc => 'Specify which columns to show, in order. Prefix with "+" or "^" to append/prepend to the default list.', }, order_by => { is => 'Text', is_optional => 1, doc => 'Output rows are listed sorted by these named columns in increasing order.', }, ], has_param => [ style => { is => 'Text', is_optional => 1, valid_values => [qw/text csv tsv pretty html xml newtext/], default_value => 'text', doc => 'The output format.', }, csv_delimiter => { is => 'Text', is_optional => 1, default_value => ',', doc => 'For the "csv" output style, specify the field delimiter for something besides a comma.', }, noheaders => { is => 'Boolean', is_optional => 1, default => 0, doc => 'Include headers. Set --noheaders to turn headers off.', }, ], has_transient => [ output => { is => 'IO::Handle', is_optional =>1, is_transient =>1, default => \*STDOUT, doc => 'output handle for list, defauls to STDOUT', }, _fields => { is_many => 1, is_optional => 1, doc => 'Methods which the caller intends to use on the fetched objects. May lead to pre-fetching the data.', }, ], doc => 'lists objects matching the specified expression', }; sub sub_command_sort_position { .2 }; sub create { my $class = shift; my $self = $class->SUPER::create(@_); #$DB::single = 1; if (defined($self->csv_delimiter) and ($self->csv_delimiter ne $self->__meta__->property_meta_for_name('csv_delimiter')->default_value) and ($self->style ne 'csv') ) { $self->error_message('--csv-delimiter is only valid when used with --style csv'); return; } unless ( ref $self->output ){ my $ofh = IO::File->new("> ".$self->output); $self->error_message("Can't open file handle to output param ".$self->output) and die unless $ofh; $self->output($ofh); } return $self; } sub _resolve_boolexpr { my $self = shift; my ($bool_expr,%extra); eval { ($bool_expr, %extra) = UR::BoolExpr->resolve_for_string( $self->subject_class_name, $self->_complete_filter, $self->_hint_string, $self->order_by, ); }; my $error = $@; unless ($bool_expr) { eval { ($bool_expr, %extra) = UR::BoolExpr->_old_resolve_for_string( $self->subject_class_name, $self->_complete_filter, $self->_hint_string, $self->order_by, ) }; if ($bool_expr) { $self->warning_message("Failed to parse your query, but it was recognized by the deprecated filter parser.\n Try putting quotes around the entire filter expression.\n Use double quotes if your filter already includes single quotes, and vice-versa.\n Values containing spaces need quotes around them as well\n The error from the parser was:\n $error"); } else { die $error if $error; } } if (%extra) { $self->error_message( sprintf( 'Cannot list for class %s because some items in the filter or show were not properties of that class: %s', $self->subject_class_name, join(', ', keys %extra) ) ) } return $bool_expr; } # Used by create() and execute() to distinguish whether an item from the show list # is likely a property of the subject class or a more complicated expression that needs # to be eval-ed later sub _show_item_is_property_name { my($self, $item) = @_; return $item =~ m/^[\w\.]+$/; } sub execute { my $self = shift; my $subject_class_name = $self->subject_class_name; # ensure classes can be loaded from whatever namespace the subject class has # TODO: make the UR command open the door for the type loading below to hit # all namespaces when _it_ is running only. The ur commands are sw maint tools. my ($ns) = ($subject_class_name =~ /^(.*?)::/); eval "use $ns"; my $subject_class = UR::Object::Type->get($subject_class_name); # Determine things to show my @fields = $self->_resolve_field_list; my $bool_expr = $self->_resolve_boolexpr(); return unless (defined $bool_expr); # TODO: instead of using an iterator, get all the results back in a list and # have the styler use the list, since it needs all the results to space the columns # out properly anyway my $iterator; unless ($iterator = $self->subject_class_name->create_iterator($bool_expr)) { $self->error_message($self->subject_class_name->error_message); return; } my $style_module_name = __PACKAGE__ . '::' . ucfirst $self->style; my $style_module = $style_module_name->new( iterator => $iterator, show => \@fields, csv_delimiter => $self->csv_delimiter, noheaders => $self->noheaders, output => $self->output, ); $style_module->format_and_print; return 1; } sub _resolve_field_list { my $self = shift; if ( my $show = $self->show ) { if (substr($show,0,1) =~ /([\+\^\-])/) { # if it starts with any of the special characters, combine with the default my $default = $self->__meta__->property('show')->default_value; unless ($default) { $default = join(",", map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name)); } $show = join(',',$default,$show); } my @show; my $expr; my @parts = (split(/,/, $show)); my $append_prepend_or_omit = '+'; my $prepend_count = 0; for my $item (@parts) { if ($item =~ /^([\+\^\-])/) { if ($1 eq '^') { $prepend_count = 0; } $append_prepend_or_omit = $1; $item = substr($item,1); } if ($self->_show_item_is_property_name($item) and not defined $expr) { if ($append_prepend_or_omit eq '+') { # append push @show, $item; } elsif ($append_prepend_or_omit eq '^') { # prepend splice(@show, $prepend_count, 0, $item); $prepend_count++; } elsif ($append_prepend_or_omit eq '-') { # omit @show = grep { $_ ne $item } @show; } else { die "unrecognized operator in show string: $append_prepend_or_omit"; } } else { if ($expr) { $expr .= ',' . $item; } else { $expr = '(' . $item; } my $o; if (eval('sub { ' . $expr . ')}')) { push @show, $expr . ')'; #print "got: $expr<\n"; $expr = undef; } } } if ($expr) { die "Bad expression: $expr\n$@\n"; } return @show; } else { return map { $_->property_name } $self->_properties_for_class_to_document($self->subject_class_name); } } sub _filter_doc { my $class = shift; my $doc = <18" # > is a special character name='Bob Jones' # spaces in a field value Standard and/or predicated logic is supported (like in SQL). "name='Bob Jones' and job='Captain' and age>18" "name='Betty Jones' and (score < 10 or score > 100)" The "like" operator uses "%" as a wildcard: "name like '%Jones'" Use square brackets for "in" clauses. "name like '%Jones' and job in [Captain,Ensign,'First Officer']" Use a dot (".") to indirectly access related data (joins): "age<18 and father.address.city='St. Louis'" "previous_order.items.price > 100" A shorthand filter form allows many queries to be written more concisely: regular: "name = 'Jones' and age between 18-25 and happy in ['yes','no','maybe']" shorthand: name~%Jones,age:18-25,happy:yes/no/maybe Shorthand Key: -------------- , " and " = exactly equal to ~ "like" the value : "between" two values, dash "-" separated : "in" the list of several values, slash "/" separated EOS if (my $help_synopsis = $class->help_synopsis) { $doc .= "\n Examples:\n ---------\n"; $doc .= " $help_synopsis\n"; } # Try to get the subject class name my $self = $class->create; if ( not $self->subject_class_name and my $subject_class_name = $self->_resolved_params_from_get_options->{subject_class_name} ) { $self = $class->create(subject_class_name => $subject_class_name); } my @properties = $self->_properties_for_class_to_document($self->subject_class_name); my @filterable_properties = grep { ! $_->data_type or index($_->data_type, '::') == -1 } @properties; my @relational_properties = grep { $_->data_type and index($_->data_type, '::') >= 0 } @properties; my $longest_name = 0; foreach my $property ( @properties ) { my $name_len = length($property->property_name); $longest_name = $name_len if ($name_len > $longest_name); } my @data; if ( ! $self->subject_class_name ) { $doc .= " Can't determine the list of properties without a subject_class_name.\n"; } elsif ( ! @properties ) { $doc .= sprintf(" %s\n", $self->error_message); } else { if (@filterable_properties) { push @data, 'Simple Properties:'; for my $property ( @filterable_properties ) { push @data, [$property->property_name, $self->_doc_for_property($property, $longest_name)]; } } if (@relational_properties) { push @data, 'Complex Properties (support dot-syntax):'; for my $property ( @relational_properties ) { my $name = $property->property_name; my @doc = $self->_doc_for_property($property,$longest_name); push @data, [$name, $doc[0]]; for my $n (1..$#doc) { push @data, ['', $doc[$n]]; } } } } my @lines = $class->_format_property_doc_data(@data); $doc .= join("\n ", @lines); $self->delete; return $doc; } sub _doc_for_property { my $self = shift; my $property = shift; my $longest_name = shift; my $doc; my $property_doc = $property->doc; unless ($property_doc) { eval { foreach my $ancestor_class_meta ( $property->class_meta->ancestry_class_metas ) { my $ancestor_property_meta = $ancestor_class_meta->property_meta_for_name($property->property_name); if ($ancestor_property_meta and $ancestor_property_meta->doc) { $property_doc = $ancestor_property_meta->doc; last; } } }; } $property_doc ||= ''; $property_doc =~ s/\n//gs; # Get rid of embeded newlines my $data_type = $property->data_type; my $data_class = eval { $property->_data_type_as_class_name }; if ($data_type and $data_class eq $data_type) { my @has = $self->_properties_for_class_to_document($data_class); my @labels; for my $pmeta (@has) { my $name = $pmeta->property_name; my $type = $pmeta->data_type; if ($type and $type =~ /::/) { push @labels, "$name\[.*\]"; } else { push @labels, $name; } } return ( ($property_doc ? $property_doc : ()), " see for more details", ' has: ' . join(", ", @labels), '', ); } else { $data_type ||= 'Text'; $data_type = (index($data_type, '::') == -1) ? ucfirst(lc $data_type) : $data_type; if ($property_doc) { $property_doc = '(' . $data_type . '): ' . $property_doc; } else { $property_doc = '(' . $data_type . ')'; } return $property_doc; } } sub _format_property_doc_data { my ($class, @data) = @_; my @names = map { $_->[0] } grep { ref $_ } @data; my $longest_name = reduce { length($a) > length($b) ? $a : $b } @names; my $w = length($longest_name); my @lines; for my $data (@data) { if (ref $data) { push @lines, sprintf(" %${w}s %s", $data->[0], $data->[1]); } else { push @lines, ' ', $data, '-' x length($data); } } return @lines; } sub _properties_for_class_to_document { my $self = shift; my $target_class_name = shift; my $target_class_meta = $target_class_name->__meta__; my @id_by = $target_class_meta->id_properties; my @props = $target_class_meta->properties; no warnings; # These final maps are to get around a bug in perl 5.8 sort # involving method calls inside the sort sub that may # do sorts of their own return map { $_->[1] } sort { $a->[1]->position_in_module_header <=> $b->[1]->position_in_module_header or $a->[0] cmp $b->[0] } map { [ $_->property_name, $_ ] } grep { substr($_->property_name, 0, 1) ne '_' and not $_->implied_by and not $_->is_transient and not $_->is_deprecated } @props; } sub _base_filter { return; } sub _complete_filter { my $self = shift; return join(',', grep { defined $_ } $self->_base_filter,$self->filter); } sub help_detail { my $self = shift; return join( "\n", $self->_style_doc, $self->_filter_doc, ); } sub _style_doc { return <_show_item_is_property_name($_) } $self->_resolve_field_list(); return join(',',@show_parts); } 1; List000755023532023421 012121654174 16257 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/CommandStyle.pm000444023532023421 2517112121654174 20100 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Object/Command/Listpackage UR::Object::Command::List::Style; our $VERSION = "0.41"; # UR $VERSION; sub new { my ($class, %args) = @_; foreach (qw/iterator show noheaders output/){ die "no value for $_!" unless defined $args{$_}; } return bless(\%args, $class); } sub _get_next_object_from_iterator { my $self = shift; my $obj; for (1) { $obj = eval { $self->{'iterator'}->next }; if ($@) { UR::Object::Command::List->warning_message($@); redo; } } return $obj; } sub _object_properties_to_string { my ($self, $o, $char) = @_; my @v; return join( $char, map { defined $_ ? $_ : '' } map { $self->_object_property_to_string($o,$_) } @{$self->{show}} ); } sub _object_property_to_string { my ($self, $o, $property) = @_; my @v; if (substr($property,0,1) eq '(') { @v = eval $property; if ($@) { @v = (''); # ($@ =~ /^(.*)$/); } } else { @v = (); foreach my $i ($o->__get_attr__($property)) { if (! defined $i) { push @v, ""; } elsif (Scalar::Util::blessed($i) and $i->isa('UR::Value') and $i->can('create_view')) { # Here we allow any UR::Values that have their own views to present themselves. my $v = $i->create_view( perspective => 'default', toolkit => 'text' ); push @v, $v->content(); } elsif (Scalar::Util::blessed($i) and $i->can('__display_name__')) { push @v, $i->__display_name__; } else { push @v, $i; } } } if (@v > 1) { no warnings; return join(' ',@v); } else { return $v[0]; } } sub format_and_print{ my $self = shift; unless ( $self->{noheaders} ) { $self->{output}->print($self->_get_header_string. "\n"); } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $self->{output}->print($self->_get_object_string($object), "\n"); $count++; } } package UR::Object::Command::List::Html; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; return "". join("", map { uc } @{$self->{show}}) .""; } sub _get_object_string{ my ($self, $object) = @_; my $out = ""; for my $property ( @{$self->{show}} ){ $out .= "" . $object->$property . ""; } return $out . ""; } sub format_and_print{ my $self = shift; $self->{output}->print(""); #cannot use super because \n screws up javascript unless ( $self->{noheaders} ) { $self->{output}->print($self->_get_header_string); } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $self->{output}->print($self->_get_object_string($object)); $count++; } $self->{output}->print("
"); } package UR::Object::Command::List::Csv; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; my $delimiter = $self->{'csv_delimiter'}; return join($delimiter, map { lc } @{$self->{show}}); } sub _get_object_string { my ($self, $object) = @_; return $self->_object_properties_to_string($object, $self->{'csv_delimiter'}); } package UR::Object::Command::List::Tsv; use base 'UR::Object::Command::List::Csv'; sub _get_header_string{ my $self = shift; my $delimiter = "\t"; return join($delimiter, map { lc } @{$self->{show}}); } sub _get_object_string { my ($self, $object) = @_; return $self->_object_properties_to_string($object, "\t"); } package UR::Object::Command::List::Pretty; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ return ''; } sub _get_object_string{ my ($self, $object) = @_; my $out; for my $property ( @{$self->{show}} ) { my $value = join(', ', $self->_object_property_to_string($object,$property)); $out .= sprintf( "%s: %s\n", Term::ANSIColor::colored($property, 'red'), Term::ANSIColor::colored($value, 'cyan'), ); } return $out; } package UR::Object::Command::List::Xml; use base 'UR::Object::Command::List::Style'; sub format_and_print{ my $self = shift; my $out; eval "use XML::LibXML"; if ($@) { die "Please install XML::LibXML (run sudo cpanm XML::LibXML) to use this tool!"; } my $doc = XML::LibXML->createDocument(); my $results_node = $doc->createElement("results"); $results_node->addChild( $doc->createAttribute("generated-at",$UR::Context::current->now()) ); $doc->setDocumentElement($results_node); my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { my $object_node = $results_node->addChild( $doc->createElement("object") ); my $object_reftype = ref $object; $object_node->addChild( $doc->createAttribute("type",$object_reftype) ); $object_node->addChild( $doc->createAttribute("id",$object->id) ); for my $property ( @{$self->{show}} ) { my $property_node = $object_node->addChild ($doc->createElement($property)); my @items = $object->$property; my $reftype = ref $items[0]; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { foreach (@items) { my $subobject_node = $property_node->addChild( $doc->createElement("object") ); $subobject_node->addChild( $doc->createAttribute("type",$reftype) ); $subobject_node->addChild( $doc->createAttribute("id",$_->id) ); #$subobject_node->addChild( $doc->createTextNode($_->id) ); #xIF } } else { foreach (@items) { $property_node->addChild( $doc->createTextNode($_) ); } } } $count++; } $self->{output}->print($doc->toString(1)); } package UR::Object::Command::List::Text; use base 'UR::Object::Command::List::Style'; sub _get_header_string{ my $self = shift; return join ( "\n", join("\t", map { uc } @{$self->{show}}), join("\t", map { '-' x length } @{$self->{show}}), ); } sub _get_object_string{ my ($self, $object) = @_; $self->_object_properties_to_string($object, "\t"); } sub format_and_print{ my $self = shift; my $tab_delimited; unless ($self->{noheaders}){ $tab_delimited .= $self->_get_header_string."\n"; } my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $tab_delimited .= $self->_get_object_string($object)."\n"; $count++; } $self->{output}->print($self->tab2col($tab_delimited)); } sub tab2col{ my ($self, $data) = @_; #turn string into 2d array of arrayrefs ($array[$rownum][$colnum]) my @rows = split("\n", $data); @rows = map { [split("\t", $_)] } @rows; my $output; my @width; #generate array of max widths per column foreach my $row_ref (@rows) { my @cols = @$row_ref; my $index = $#cols; for (my $i = 0; $i <= $index; $i++) { my $l = (length $cols[$i]) + 3; #TODO test if we need this buffer space $width[$i] = $l if ! defined $width[$i] or $l > $width[$i]; } } #create a array of blanks to use as a templatel my @column_template = map { ' ' x $_ } @width; #iterate through rows and cols, substituting in the row entry in your template foreach my $row_ref (@rows) { my @cols = @$row_ref; my $index = $#cols; #only apply template for all but the last entry in a row for (my $i = 0; $i < $index; $i++) { my $entry = $cols[$i]; my $template = $column_template[$i]; substr($template, 0, length $entry, $entry); $output.=$template; } $output.=$cols[$index]."\n"; #Don't need traling spaces on the last entry } return $output; } package UR::Object::Command::List::Newtext; use base 'UR::Object::Command::List::Text'; sub format_and_print{ my $self = shift; my $tab_delimited; unless ($self->{noheaders}){ $tab_delimited .= $self->_get_header_string."\n"; } my $view = UR::Object::View->create( subject_class_name => 'UR::Object', perspective => 'lister', toolkit => 'text', aspects => [ @{$self->{'show'}} ], ); my $count = 0; while (my $object = $self->_get_next_object_from_iterator()) { $view->subject($object); $tab_delimited .= $view->content() . "\n"; $count++; } $self->{output}->print($self->tab2col($tab_delimited)); } 1; =pod =head1 NAME UR::Object::Command::List - Fetches and lists objects in different styles. =head1 SYNOPSIS package MyLister; use strict; use warnings; use above "UR"; class MyLister { is => 'UR::Object::Command::List', has => [ # add/modify properties ], }; 1; =head1 Provided by the Developer =head2 subject_class_name (optional) The subject_class_name is the class for which the objects will be fetched. It can be specified one of two main ways: =over =item I For this do nothing, the end user will have to provide it when the command is run. =item I For this, in the class declaration, add a has key w/ arrayref of hashrefs. One of the hashrefs needs to be subject_class_name. Give it this declaration: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ subject_class_name => { value => , is_constant => 1, }, ], }; =back =head2 show (optional) Add defaults to the show property: class MyFetchAndDo { is => 'UR::Object::Command::FetchAndDo', has => [ show => { default_value => 'name,age', }, ], }; =head2 helps (optional) Overwrite the help_brief, help_synopsis and help_detail methods to provide specific help. If overwiting the help_detail method, use call '_filter_doc' to get the filter documentation and usage to combine with your specific help. =head1 List Styles text, csv, html, xml, pretty (inprogress) =cut #$HeadURL: svn+ssh://svn/srv/svn/gscpan/perl_modules/trunk/UR/Object/Command/List.pm $ #$Id: List.pm 50329 2009-08-25 20:10:00Z abrummet $ Namespace000755023532023421 012121654175 14455 5ustar00abrummetgsc000000000000UR-0.41/lib/URCommand.pm.opts000444023532023421 2307512121654173 17537 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace$UR::Namespace::Command::OPTS_SPEC = [ '>define', [ '>class', [ 'extends=s', 'files', 'names=s@', 'files', 'help!', undef ], '>datasource', [ '>file', [ 'server=s', 'files', 'singleton!', undef, 'dsid=s', 'files', 'dsname=s', 'files', 'help!', undef ], '>mysql', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'dsid=s', 'files', 'server=s', 'files', 'dsname=s', 'files', 'help!', undef ], '>oracle', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'dsid=s', 'files', 'server=s', 'files', 'dsname=s', 'files', 'help!', undef ], '>pg', [ 'auth=s', 'files', 'login=s', 'files', 'nosingleton!', undef, 'owner=s', 'files', 'dsid=s', 'files', 'server=s', 'files', 'dsname=s', 'files', 'help!', undef ], '>sqlite', [ 'nosingleton!', undef, 'dsid=s', 'files', 'server=s', 'files', 'dsname=s', 'files', 'help!', undef ], 'help!', undef ], '>db', [ 'uri=s', 'files', 'name=s', 'files', 'help!', undef ], '>namespace', [ 'nsname=s', 'files', 'help!', undef ], 'help!', undef ], '>describe', [ 'classes-or-modules=s@', 'files', 'help!', undef ], '>init', [ 'namespace=s', 'files', 'db=s', 'files', 'help!', undef ], '>list', [ '>classes', [ 'classes-or-modules=s@', 'files', 'help!', undef ], '>modules', [ 'classes-or-modules=s@', 'files', 'help!', undef ], '>objects', [ 'subject-class-name=s', 'files', 'csv-delimiter=s', 'files', 'filter=s', 'files', 'noheaders!', undef, 'show=s', 'files', 'style=s', 'files', 'help!', undef ], 'help!', undef ], '>old', [ '>diff-rewrite', [ 'help!', undef ], '>diff-update', [ 'help!', undef ], '>export-dbic-classes', [ 'bare-args=s@', 'files', 'classes-or-modules=s@', 'files', 'help!', undef ], '>info', [ 'subject=s@', 'files', 'help!', undef ], '>redescribe', [ 'classes-or-modules=s@', 'files', 'help!', undef ], 'help!', undef ], '>sys', [ '>class-browser', [ 'help!', undef ], 'help!', undef ], '>test', [ '>callcount', [ '>list', [ 'file=s', 'files', 'show=s', 'files', 'csv-delimiter=s', 'files', 'filter=s', 'files', 'noheaders!', undef, 'style=s', 'files', 'help!', undef ], 'help!', undef ], '>compile', [ 'classes-or-modules=s@', 'files', 'help!', undef ], '>eval', [ 'bare-args=s@', 'files', 'help!', undef ], '>run', [ 'color!', undef, 'junit!', undef, 'list!', undef, 'lsf!', undef, 'recurse!', undef, 'callcount!', undef, 'cover=s', undef, 'cover-cvs-changes!', undef, 'cover-svk-changes!', undef, 'cover-svn-changes!', undef, 'coverage!', undef, 'inc=s@', 'files', 'jobs=s', undef, 'long!', undef, 'lsf-params=s', 'files', 'noisy!', undef, 'perl-opts=s', 'files', 'run-as-lsf-helper=s', 'files', 'script-opts=s', 'files', 'time=s', 'files', 'bare-args=s@', 'files', 'help!', undef ], '>track-object-release', [ 'file=s', 'files', 'help!', undef ], '>use', [ 'exec=s', 'files', 'summarize-externals!', undef, 'verbose!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>window', [ 'src=s@', 'files', 'help!', undef ], 'help!', undef ], '>update', [ '>class-diagram', [ 'file=s', 'files', 'data-source=s', 'files', 'depth=s', undef, 'include-ur-object!', undef, 'show-attributes!', undef, 'show-methods!', undef, 'initial-name=s@', 'files', 'help!', undef ], '>classes-from-db', [ 'class-name=s', undef, 'data-source=s', undef, 'force-check-all-tables!', undef, 'force-rewrite-all-classes!', undef, 'table-name=s', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>pod', [ 'output-path=s', 'files', 'base-commands=s@', 'files', 'help!', undef ], '>rename-class', [ 'force!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>rewrite-class-header', [ 'force!', undef, 'classes-or-modules=s@', 'files', 'help!', undef ], '>schema-diagram', [ 'file=s', 'files', 'data-source=s', 'files', 'depth=s', undef, 'show-columns!', undef, 'initial-name=s@', 'files', 'help!', undef ], '>tab-completion-spec', [ 'output=s', 'files', 'classname=s', 'files', 'help!', undef ], 'help!', undef ], 'help!', undef ]; Command.pm000444023532023421 51712121654175 16511 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespacepackage UR::Namespace::Command; # This is the module behind the "ur" executable. use strict; use warnings; use UR; use UR::Namespace::Command::Base; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', doc => 'tools to create and maintain a ur class tree' ); 1; Command000755023532023421 012121654175 16033 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/NamespaceUpdate.pm000444023532023421 50012121654174 17722 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Update; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => 'update parts of the source tree of a UR namespace' ); sub sub_command_sort_position { 4 } 1; Sys.pm000444023532023421 36712121654174 17271 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Sys; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => 'service launchers' ); 1; Init.pm000444023532023421 342612121654174 17435 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Init; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "Command", has => [ namespace => { is => 'Text', shell_args_position => 1, doc => 'the name of the namespace/app to create' }, db => { is => 'Text', is_optional => 1, shell_args_position => 2, doc => 'the (optional) DBI connection string for the primary data source', }, ], doc => 'create a new ur app with default classes in place', ); sub sub_command_sort_position { 1 } sub execute { my $self = shift; my $c; my $t = UR::Context::Transaction->begin(); $self->status_message("*** ur define namespace " . $self->namespace); UR::Namespace::Command::Define::Namespace->execute(nsname => $self->namespace)->result or die; if ($self->db) { $self->status_message("\n*** cd " . $self->namespace); chdir $self->namespace or ($self->error_message("error changing to namespace dir? $!") and die); $self->status_message("\n*** ur define db " . $self->db); $c = UR::Namespace::Command::Define::Db->create(uri => $self->db) or return; $c->dump_status_messages(1); $c->execute() or die; $self->status_message("\n*** ur update classes-from-db"); $c = UR::Namespace::Command::Update::ClassesFromDb->create(); $c->dump_status_messages(1); $c->execute() or die; } else { $self->status_message("next: cd " . $self->namespace); $self->status_message("then: ur define db DSN"); $self->status_message("then: ur update classes-from-db"); } $t->commit; return 1; } 1; RunsOnModulesInTree.pm000444023532023421 1400712121654174 22433 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command # Abstract base command for commands which run on all or part of a class tree. package UR::Namespace::Command::RunsOnModulesInTree; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ classes_or_modules => { is_many => 1, is_optional => 1, shell_args_position => 99 } ] ); sub is_abstract { my $self = shift; my $class = ref($self) || $self; return 1 if $class eq __PACKAGE__; return; } sub _help_detail_footer { my $text = return <namespace_name; unless ($namespace) { die "This command can only be run from a directory tree under a UR namespace module.\n"; } my @subject_list = $self->classes_or_modules; if ($self->can("for_each_class_object") ne __PACKAGE__->can("for_each_class_object")) { my @classes = $self->_class_objects_in_tree(@subject_list); unless ($self->before(\@classes)) { print STDERR "Terminating.\n"; return; } for my $class (@classes) { unless ($self->for_each_class_object($class)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_class_name") ne __PACKAGE__->can("for_each_class_name")) { my @class_names = $self->_class_names_in_tree(@subject_list); unless ($self->before(\@class_names)) { print STDERR "Terminating.\n"; return; } for my $class (@class_names) { unless ($self->for_each_class_name($class)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_module_file") ne __PACKAGE__->can("for_each_module_file")) { my @modules = $self->_modules_in_tree(@subject_list); unless ($self->before(\@modules)) { print STDERR "Terminating.\n"; return; } for my $module (@modules) { unless ($self->for_each_module_file($module)) { print STDERR "Terminating...\n"; return; } } } elsif ($self->can("for_each_module_file_in_parallel") ne __PACKAGE__->can("for_each_module_file_in_parallel")) { my @modules = $self->_modules_in_tree(@subject_list); unless ($self->before(\@modules)) { print STDERR "Terminating.\n"; return; } my $bucket_count = 10; my @buckets; my %child_processes; for my $bucket_number (0..$bucket_count-1) { $buckets[$bucket_number] ||= []; } while (@modules) { for my $bucket_number (0..$bucket_count-1) { my $module = shift @modules; last if not $module; push @{ $buckets[$bucket_number] }, $module; } } for my $bucket (@buckets) { my $child_pid = fork(); if ($child_pid) { # the parent process continues forking... $child_processes{$child_pid} = 1; } else { # the child process does handles its bucket for my $module (@$bucket) { unless ($self->for_each_module_file_in_parallel($module)) { exit 1; } } # and then exits quietly exit 0; } } #$DB::single = 1; while (keys %child_processes) { my $child_pid = wait(); if ($child_pid == -1) { print "lost children? " . join(" ", keys %child_processes); } delete $child_processes{$child_pid}; } } else { die "$self does not implement: for_each_[class_object|class_name|module_file]!"; } unless ($self->after()) { print STDERR "Terminating.\n"; return; } return 1; } sub before { return 1; } sub for_each_module_file { die "The for_each_module_file method is not defined by/in " . shift; } sub for_each_class_name { die "The for_each_class_name method is not defined by/in " . shift; } sub for_each_class_object { Carp::confess "The for_each_class_object method is not defined by/in " . shift; } sub after { return 1; } sub loop_methods { my $self = shift; my @methods; for my $method (qw/ for_each_class_object for_each_class_name for_each_module_file for_each_module_file_in_parallel /) { no warnings; if ($self->can($method) ne __PACKAGE__->can($method)) { push @methods, $method; } } return @methods; } sub shell_args_description { my $self = shift; my @loop_methods = $self->loop_methods; my $takes_classes = 1 if grep { /class/ } @loop_methods; my $takes_modules = 1 if grep { /modul/ } @loop_methods; my $text; if ($takes_classes and $takes_modules) { $text = "[CLASS|MODULE] [CLASS|MODULE] ..."; } elsif ($takes_classes) { $text = "[CLASS] [CLASS].."; } elsif ($takes_modules) { $text = "[MODULE] [MODULE] ..."; } else { $text = ""; } $text .= " " . $self->SUPER::shell_args_description(@_); if ($self->is_sub_command_delegator) { my @names = $self->sub_command_names; return "[" . join("|",@names) . "] $text" } return $text; } 1; Show.pm000444023532023421 27512121654175 17432 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Show; use strict; use warnings; class UR::Namespace::Command::Show { is => 'Command::Tree', doc => 'show data about classes, data storage', }; 1; List.pm000444023532023421 45312121654175 17423 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::List; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => "list objects, classes, modules" ); sub sub_command_sort_position { 5 } 1; Define.pm000444023532023421 55712121654175 17707 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Define; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => "define namespaces, data sources and classes", ); sub sub_command_sort_position { 2 } sub shell_args_description { "[namespace|...]"; } 1; Old.pm000444023532023421 56612121654175 17233 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Old; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', doc => "deprecated commands for namespaces, data sources and classes", ); sub _is_hidden_in_docs { 1 } sub shell_args_description { "[namespace|...]"; } 1; Base.pm000444023532023421 3153612121654175 17430 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Commandpackage UR::Namespace::Command::Base; use strict; use warnings; use UR; use Cwd; use Carp; use File::Find; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command::V1', is_abstract => 1, has => [ namespace_name => { type => 'String', is_optional => 1, doc => 'Name of the Namespace to work in. Auto-detected if within a Namespace directory' }, lib_path => { type => "FilesystemPath", doc => "The directory in which the namespace module resides. Auto-detected normally.", is_constant => 1, calculate_from => ['namespace_name'], calculate => q( # the namespace module should have gotten loaded in create() my $namespace_module = $namespace_name; $namespace_module =~ s#::#/#g; my $namespace_path = Cwd::abs_path($INC{$namespace_module . ".pm"}); unless ($namespace_path) { Carp::croak("Namespace module $namespace_name has not been loaded yet"); } $namespace_path =~ s/\/[^\/]+.pm$//; return $namespace_path; ), }, working_subdir => { type => "FilesystemPath", doc => 'The current working directory relative to lib_path', calculate => q( my $lib_path = $self->lib_path; return UR::Util::path_relative_to($lib_path, Cwd::abs_path(Cwd::getcwd)); ), }, namespace_path => { type => 'FilesystemPath', doc => "The directory under which all the namespace's modules reside", is_constant => 1, calculate_from => ['namespace_name'], calculate => q( my $lib_path = $self->lib_path; return $lib_path . '/' . $namespace_name; ), }, verbose => { type => "Boolean", is_optional => 1, doc => "Causes the command to show more detailed output." }, ], doc => 'a command which operates on classes/modules in a UR namespace directory' ); sub create { my $class = shift; my ($rule,%extra) = $class->define_boolexpr(@_); my($namespace_name, $lib_path); if ($rule->specifies_value_for('namespace_name')) { $namespace_name = $rule->value_for('namespace_name'); $lib_path = $class->resolve_lib_path_for_namespace_name($namespace_name); } else { ($namespace_name,$lib_path) = $class->resolve_namespace_name_from_cwd(); unless ($namespace_name) { $class->error_message("Could not determine namespace name."); $class->error_message("Run this command from within a namespace subdirectory or use the --namespace-name command line option"); return; } $rule = $rule->add_filter(namespace_name => $namespace_name); } # Use the namespace. $class->status_message("Loading namespace module $namespace_name") if ($rule->value_for('verbose')); # Ensure the right modules are visible to the command. # Make the module accessible. # We'd like to "use lib" this directory, but any other -I/use-lib # requests should still come ahead of it. This requires a little munging. # Find the first thing in the compiled_inc list that exists my $compiled = ''; for my $path ( UR::Util::compiled_inc() ) { next unless -d $path; $compiled = Cwd::abs_path($path); last if defined $compiled; } my $perl5lib = ''; foreach my $path ( split(':', $ENV{'PERL5LIB'}) ) { next unless -d $path; $perl5lib = Cwd::abs_path($path); last if defined $perl5lib; } my $i; for ($i = 0; $i < @INC; $i++) { # Find the index in @INC that's the first thing in # compiled-in module paths # # since abs_path returns undef for non-existant dirs, # skip the comparison if either is undef my $inc = Cwd::abs_path($INC[$i]); next unless defined $inc; last if ($inc eq $compiled or $inc eq $perl5lib); } splice(@INC, $i, 0, $lib_path); eval "use $namespace_name"; if ($@) { $class->error_message("Error using namespace module '$namespace_name': $@"); return; } my $self = $class->SUPER::create($rule); return unless $self; unless (eval { UR::Namespace->get($namespace_name) }) { $self->error_message("Namespace '$namespace_name' was not found"); return; } if ($namespace_name->can("_set_context_for_schema_updates")) { $namespace_name->_set_context_for_schema_updates(); } return $self; } sub command_name { my $class = shift; return "ur" if $class eq __PACKAGE__; my $name = $class->SUPER::command_name; $name =~ s/^u-r namespace/ur/; return $name; } sub help_detail { return shift->help_brief } # Return a list of module pathnames relative to lib_path sub _modules_in_tree { my $self = shift; my @modules; my $lib_path = $self->lib_path; my $namespace_path = $self->namespace_path; my $wanted_closure = sub { if (-f $_ and m/\.pm$/) { push @modules, UR::Util::path_relative_to($lib_path, $_); } }; unless (@_) { File::Find::find({ no_chdir => 1, wanted => $wanted_closure, }, $namespace_path); } else { # this method takes either module paths or class names as params # normalize to module paths NAME: for (my $i = 0; $i < @_; $i++) { my $name = $_[$i]; if ($name =~ m/::/) { # It's a class name my @name_parts = split(/::/, $name); unless ($self->namespace_name eq $name_parts[0]) { $self->warning_message("Skipping class name $name: Not in namespace ".$self->namespace_name); next NAME; } $name = join('/', @name_parts) . ".pm"; } # First, check the pathname relative to the cwd CHECK_LIB_PATH: foreach my $check_name ( $name, $lib_path.'/'.$name, $namespace_path.'/'.$name) { if (-e $check_name) { if (-f $check_name and $check_name =~ m/\.pm$/) { push @modules, UR::Util::path_relative_to($lib_path, $check_name); next NAME; # found it, don't check the other $check_name } elsif (-d $check_name) { File::Find::find({ no_chdir => 1, wanted => $wanted_closure, }, $check_name); } elsif (-e $check_name) { $self->warning_message("Ignoring non-module $check_name"); next CHECK_LIB_PATH; } } } } } return @modules; } sub _class_names_in_tree { my $self = shift; my @modules = $self->_modules_in_tree(@_); my $lib_path = $self->lib_path; my @class_names; for my $module (@modules) { my $class = $module; $class =~ s/^$lib_path\///; $class =~ s/\//::/g; $class =~ s/\.pm$//; push @class_names, $class; } return @class_names; } sub _class_objects_in_tree { my $self = shift; my @class_names = $self->_class_names_in_tree(@_); my @class_objects; for my $class_name (sort { uc($a) cmp uc($b) } @class_names) { unless(UR::Object::Type->use_module_with_namespace_constraints($class_name)) { #if ($@) { print STDERR "Failed to use class $class_name!\n"; print STDERR $@,"\n"; next; } my $c = UR::Object::Type->is_loaded(class_name => $class_name); unless ($c) { #print STDERR "Failed to find class object for class $class_name\n"; next; } push @class_objects, $c; #print $class_name,"\n"; } return @class_objects; } # Tries to guess what namespace you are in from your current working # directory. When called in list context, it also returns the directroy # name the namespace module was found in sub resolve_namespace_name_from_cwd { my $class = shift; my $cwd = shift; $cwd ||= Cwd::cwd(); my @lib = grep { length($_) } split(/\//,$cwd); SUBDIR: while (@lib) { my $namespace_name = pop @lib; my $lib_path = "/" . join("/",@lib); my $namespace_module_path = $lib_path . '/' . $namespace_name . '.pm'; if (-e $namespace_module_path) { if ($class->_is_file_the_namespace_module($namespace_name, $namespace_module_path)) { if (wantarray) { return ($namespace_name, $lib_path); } else { return $namespace_name; } } } } return; } # Returns true if the given file is the namespace module we're looking for. # The only certain way is to go ahead and load it, but this should be good # enough for ligitimate use cases. sub _is_file_the_namespace_module { my($class,$namespace_name,$namespace_module_path) = @_; my $fh = IO::File->new($namespace_module_path); return unless $fh; while (my $line = $fh->getline) { if ($line =~ m/package\s+$namespace_name\s*;/) { # At this point $namespace_name should be a plain word with no ':'s # and if the file sets the package to a single word with no colons, # it's pretty likely that it's a namespace module. return 1; } } return; } # Return the pathname that the specified namespace module can be found sub resolve_lib_path_for_namespace_name { my($class,$namespace_name,$cwd) = @_; unless ($namespace_name) { Carp::croak('namespace name is a required argument for UR::Util::resolve_lib_path_for_namespace_name()'); } # first, see if we're in a namespace dir my($resolved_ns_name, $lib_path ) = $class->resolve_namespace_name_from_cwd($cwd); return $lib_path if (defined($resolved_ns_name) and $resolved_ns_name eq $namespace_name); foreach $lib_path ( @main::INC ) { my $expected_namespace_module = $lib_path . '/' . $namespace_name . '.pm'; $expected_namespace_module =~ s/::/\//g; # swap :: for / if ( $class->_is_file_the_namespace_module($namespace_name, $expected_namespace_module)) { return $lib_path; } } return; } 1; =pod =head1 NAME UR::Namespace::Command - Top-level Command module for the UR namespace commands =head1 DESCRIPTION This class is the parent class for all the namespace-manipluation command modules, and the root for command handling behind the 'ur' command-line script. There are several sub-commands for manipluating a namespace's metadata. =over 4 =item browser Start a lightweight web server for viewing class and schema information =item commit Update data source schemas based on the current class structure =item define Define metadata instances such as classes, data sources or namespaces =item describe Get detailed information about a class =item diff Show a diff for various kinds of other ur commands. =item info Show brief information about class or schema metadata =item list List various types of things =item redescribe Outputs class description(s) formatted to the latest standard =item rename Rename logical schema elements. =item rewrite Rewrites class descriptions headers to normalize manual changes. =item test Sub-commands related to testing =item update Update metadata based on external data sources =back Some of these commands have sub-commands of their own. You can get more detailed information by typing 'ur --help' at the command line. =head1 SEE ALSO Command, UR, UR::Namespace =cut Test.pm000444023532023421 52712121654175 17431 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command # The diff command delegates to sub-commands under the adjoining directory. package UR::Namespace::Command::Test; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", doc => 'tools for testing and debugging', ); 1; Show000755023532023421 012121654174 16752 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandSubclasses.pm000444023532023421 1552112121654172 21576 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Subclasses; use strict; use warnings; use UR; use YAML; my $spacing = ''; class UR::Namespace::Command::Show::Subclasses { is => 'Command::V2', has=> [ superclass => { is => 'Text', shell_args_position => 1, doc => 'Only show subclasses of this class.', }, color => { is => 'Boolean', is_optional => 1, default_value => 1, doc => 'Display in color.', }, maximum_depth => { is => 'Int', is_optional => 1, default_value => -1, doc => 'Maximum subclass depth. Negative means infinite.', }, recalculate => { is => 'Boolean', is_optional => 1, default_value => 0, doc => 'Recreate the cache instead of using the results of a previous run.', }, flat => { is => 'Boolean', is_optional => 1, doc => 'Simply prints the subclass names with no other formatting or coloring.', } ], doc => 'Display subclasses of a given class.', }; sub help_synopsis { my $self = shift; my $result .= < ur show subclasses EOP return $result; } sub _mine_tree_for_class { my ($tree, $name, $result) = @_; if(ref($tree) eq 'HASH') { for my $key (keys %{$tree}) { if($key eq $name) { push(@{$result}, 1); } else { _mine_tree_for_class($tree->{$key}, $name, $result); } } } elsif(ref($tree) eq 'ARRAY') { for my $item (@{$tree}) { if($item eq $name) { push(@{$result}, 1); } _mine_tree_for_class($item, $name, $result); } } return; } sub execute { my ($self) = @_; my $indexfile = '/tmp/.ur_class_index'; my $subclass_index_ref; if($self->recalculate or (not -e $indexfile)) { my $test_use_cmd = UR::Namespace::Command::Test::Use->create(); $test_use_cmd->execute(); $subclass_index_ref = {}; create_subclass_index('UR::Object', $subclass_index_ref); my %subclass_index = %{$subclass_index_ref}; open(my $output_fh, '>', $indexfile); for my $key (keys %subclass_index) { print $output_fh sprintf("%s %s\n", $key, join("\t", @{$subclass_index{$key}})); } close($output_fh); } else { $subclass_index_ref = parse_subclass_index_file($indexfile); } # check to see if superclass is even in the subclass_index my @result; _mine_tree_for_class($subclass_index_ref, $self->superclass, \@result); unless(@result) { my $class_name = $self->color ? Term::ANSIColor::colored($self->superclass, 'red') : $self->superclass; printf "%s is not a valid class, check your spelling or " . "see --help (recalculate).\n", $class_name; return; } if($self->flat) { $self->display_subclasses_flat($subclass_index_ref, $self->superclass, 0) } else { $self->display_subclasses($subclass_index_ref, $self->superclass, '', ' ', 0); } return 1; } sub create_subclass_index { my ($seed, $index_ref) = @_; my @children = $seed->__meta__->subclasses_loaded; for my $child (@children) { my @parents = @{$child->__meta__->{is}}; for my $parent (@parents) { if($index_ref->{$parent}) { push(@{$index_ref->{$parent}}, $child); } else { $index_ref->{$parent} = [$child]; } } } } sub parse_subclass_index_file { my ($indexfile) = @_; open(IN, '<', $indexfile); my %index; while(my $line = ) { chomp($line); if($line) { my ($parent, $rest) = split(/ /, $line); if($rest) { my @children = split('\t', $rest); $index{$parent} = \@children; } else { $index{$parent} = []; } } } return \%index } sub display_subclasses_flat { my ($self, $index_ref, $name, $depth) = @_; my $maximum_depth = $self->maximum_depth; if($depth == $maximum_depth + 1 and $maximum_depth != -1) { return; } print "$name\n"; # get the children my $children_ref = $index_ref->{$name}; my @children; if($children_ref) { @children = @{$index_ref->{$name}}; } else { # if it isn't in index it has no children. @children = (); } # loop over children for my $child (@children) { $self->display_subclasses_flat($index_ref, $child, $depth+1); } } sub display_subclasses { my ($self, $index_ref, $name, $global_prefix, $personal_prefix, $depth) = @_; my $maximum_depth = $self->maximum_depth; my ($dgp, $dpp, $dn) = ($global_prefix, $personal_prefix, $name); if($self->color) { ($dgp, $dpp, $dn) = colorize_output($global_prefix, $personal_prefix, $name, $self->superclass); } print join('', $dgp, $dpp, $spacing, $dn); my $o = ($personal_prefix =~ /^\|/ ) ? '|' : ' '; my $child_global_prefix = sprintf("%s%s %s", $global_prefix, $o, $spacing); # get the children my $children_ref = $index_ref->{$name}; my @children; if($children_ref) { @children = @{$index_ref->{$name}}; } else { # if it isn't in index it has no children. @children = (); } # loop over children my $len_children = scalar(@children); if($len_children and $depth == $maximum_depth and $maximum_depth != -1) { print " ...\n"; return; } print "\n"; my $i = 1; for my $child (@children) { my $child_personal_prefix = ($len_children == $i) ? '`-' : '|-'; $self->display_subclasses($index_ref, $child, $child_global_prefix, $child_personal_prefix, $depth+1); $i += 1; } } sub colorize_output { my ($global_prefix, $personal_prefix, $name, $superclass) = @_; my $dgp = Term::ANSIColor::colored($global_prefix, 'white'); my $dpp = Term::ANSIColor::colored($personal_prefix, 'white'); my $name_prefix = $name; if($name_prefix =~ /^($superclass)/) { $name_prefix = $superclass; } else { $name_prefix = ''; } my $name_suffix = $name; $name_suffix =~ s/^($superclass)//; my $dn = sprintf("%s%s", Term::ANSIColor::colored($name_prefix, 'white'), $name_suffix ); return ($dgp, $dpp, $dn); } 1; Properties.pm000444023532023421 535712121654173 21612 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Properties; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ classes_or_modules => { is_optional => 0, is_many => 1, shell_args_position => 99, doc => 'classes to describe by class name or module path', }, ], doc => 'show class properties, relationships, meta-data', ); sub sub_command_sort_position { 3 } sub help_synopsis { return <create_view( perspective => 'default', toolkit => 'text', aspects => [ 'namespace', 'table_name', 'data_source_id', 'is_abstract', 'is_final', 'is_singleton', 'is_transactional', 'schema_name', 'meta_class_name', { label => 'Inherits from', name => 'ancestry_class_names', }, { label => 'Properties', name => 'properties', subject_class_name => 'UR::Object::Property', perspective => 'description line item', toolkit => 'text', aspects => ['is_id', 'property_name', 'column_name', 'data_type', 'is_optional' ], }, { label => "References", name => 'all_id_by_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'reference description', toolkit => 'text', aspects => [], }, { label => "Referents", name => 'all_reverse_as_property_metas', subject_class_name => 'UR::Object::Property', perspective => 'reference description', toolkit => 'text', aspects => [], }, ], ); unless ($view) { $self->error_message("Can't initialize view"); return; } $view->subject($class_meta); $view->show(); } 1; Schema.pm000444023532023421 302312121654174 20643 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Showpackage UR::Namespace::Command::Show::Schema; use strict; use warnings; class UR::Namespace::Command::Show::Schema { is => 'Command::V2', has_input => [ class_names => { is => 'Text', is_many => 1, shell_args_position => 1, require_user_verify => 0, doc => 'dump the required database schema changes for a class or classes' }, complete => { is => 'Boolean', default_value => 0, doc => 'when set, dump the complete table creation command not just the required changes', }, ], doc => 'database DDL', }; sub execute { my $self = shift; my @class_names = $self->class_names; $ENV{UR_DBI_NO_COMMIT} = 1; my $t = UR::Context::Transaction->begin; $DB::single = 1; for my $class_name (@class_names) { my $meta = $class_name->__meta__; my $class_name = $meta->class_name; $self->status_message("-- class $class_name\n"); my $ds = $meta->data_source; my @schema_objects = $ds->generate_schema_for_class_meta($meta,1); my ($tt) = grep { $_->isa("UR::DataSource::RDBMS::Table") } @schema_objects; my @ddl = $ds->_resolve_ddl_for_table($tt, all => 1); if (@ddl) { my $ddl = join("\n",@ddl); $self->status_message($ddl); } else { $self->status_message("-- no changes for $class_name, run with the 'complete' flag for the full table DDL"); } } $t->rollback; return 1; } 1; Update000755023532023421 012121654175 17255 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandClassDiagram.pm000444023532023421 3236212121654172 22325 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::ClassDiagram; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ data_source => {type => 'String', doc => 'Which datasource to use', is_optional => 1}, depth => { type => 'Integer', doc => 'Max distance of related classes to include. Default is 1. 0 means show only the named class(es), -1 means to include everything', is_optional => 1}, file => { type => 'String', doc => 'Pathname of the Umlet (.uxf) file' }, show_attributes => { type => 'Boolean', is_optional => 1, default => 1, doc => 'Include class attributes in the diagram' }, show_methods => { type => 'Boolean', is_optional => 1, default => 0, doc => 'Include methods in the diagram (not implemented yet' }, include_ur_object => { type => 'Boolean', is_optional => 1, default => 0, doc => 'Include UR::Object and UR::Entity in the diagram (default = no)' }, initial_name => { is_many => 1, is_optional => 1, shell_args_position => 1 } ], ); sub sub_command_sort_position { 4 }; sub help_brief { "Update an Umlet diagram based on the current class definitions" } sub help_detail { return < 800; sub execute { my $self = shift; my $params = shift; #$DB::single = 1; my $namespace = $self->namespace_name; eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } my @initial_name_list = $self->initial_name; my $diagram; if (-f $params->{'file'}) { $params->{'depth'} = 0 unless (exists $params->{'depth'}); # Default is just update what's there $diagram = UR::Object::Umlet::Diagram->create_from_file($params->{'file'}); push @initial_name_list, map { $_->subject_id } UR::Object::Umlet::Class->get(diagram_name => $diagram->name); } else { $params->{'depth'} = 1 unless exists($params->{'depth'}); $diagram = UR::Object::Umlet::Diagram->create(name => $params->{'file'}); } # FIXME this can get removed when attribute defaults work correctly unless (exists $params->{'show_attributes'}) { $self->show_attributes(1); } my @involved_classes; foreach my $class_name ( @initial_name_list ) { push @involved_classes, UR::Object::Type->get(class_name => $class_name); } push @involved_classes, $self->_get_related_classes_via_inheritance( names => \@initial_name_list, depth => $params->{'depth'}, ); push @involved_classes, $self->_get_related_classes_via_properties( #names => [ map { $_->class_name } @involved_classes ], names => \@initial_name_list, depth => $params->{'depth'}, ); my %involved_class_names = map { $_->class_name => $_ } @involved_classes; # The initial placement, and how much to move over for the next box my($x_coord, $y_coord, $x_inc, $y_inc) = (20,20,40,40); my @objs = sort { $b->y <=> $a->y or $b->x <=> $a->x } UR::Object::Umlet::Class->get(); if (@objs) { my $maxobj = $objs[0]; $x_coord = $maxobj->x + $maxobj->width + $x_inc; $y_coord = $maxobj->y + $maxobj->height + $y_inc; } # First, place all the classes my @all_boxes = UR::Object::Umlet::Class->get( diagram_name => $diagram->name ); foreach my $class ( values %involved_class_names ) { my $umlet_class = UR::Object::Umlet::Class->get(diagram_name => $diagram->name, subject_id => $class->class_name); my $created = 0; unless ($umlet_class) { $created = 1; $umlet_class = UR::Object::Umlet::Class->create( diagram_name => $diagram->name, subject_id => $class->class_name, label => $class->class_name, x => $x_coord, y => $y_coord, ); # add the attributes if ($self->show_attributes) { my $attributes = $umlet_class->attributes || []; my %attributes_already_in_diagram = map { $_->{'name'} => 1 } @{ $attributes }; my %id_properties = map { $_ => 1 } $class->id_property_names; my $line_count = scalar @$attributes; foreach my $property_name ( $class->direct_property_names ) { next if $attributes_already_in_diagram{$property_name}; $line_count++; my $property = UR::Object::Property->get(class_name => $class->class_name, property_name => $property_name); push @$attributes, { is_id => $id_properties{$property_name} ? '+' : ' ', name => $property_name, type => $property->data_type, line => $line_count, }; } $umlet_class->attributes($attributes); } if ($self->show_methods) { # Not implemented yet # Use the same module the schemabrowser uses to get that info } # Make sure this box dosen't overlap other boxes while(my $overlapped = $umlet_class->is_overlapping(@all_boxes) ) { if ($umlet_class->x > MAX_X_AUTO_POSITION) { $umlet_class->x(20); $umlet_class->y( $umlet_class->y + $y_inc); } else { $umlet_class->x( $overlapped->x + $overlapped->width + $x_inc ); } } push @all_boxes, $umlet_class; } if ($created) { $x_coord = $umlet_class->x + $umlet_class->width + $x_inc; if ($x_coord > MAX_X_AUTO_POSITION) { $x_coord = 20; $y_coord += $y_inc; } } } # Next, connect the classes together foreach my $class ( values %involved_class_names ) { my @properties = grep { $_->is_delegated and $_->data_type} $class->all_property_metas(); foreach my $property ( @properties ) { next unless (exists $involved_class_names{$property->data_type}); my @property_links = eval { $property->get_property_name_pairs_for_join }; next unless @property_links; my $id_by = join(':', map { $_->[0] } @property_links); my $their_id_by = join (':', map { $_->[1] } @property_links); my $umlet_relation = UR::Object::Umlet::Relation->get( diagram_name => $diagram->name, from_entity_name => $property->class_name, to_entity_name => $property->data_type, from_attribute_name => $id_by, to_attribute_name => $their_id_by, ); unless ($umlet_relation) { $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<-', from_entity_name => $property->class_name, to_entity_name => $property->data_type, from_attribute_name => $id_by, to_attribute_name => $their_id_by, ); unless ($umlet_relation->connect_entity_attributes()) { # This didn't link to anything on the diagram $umlet_relation->delete; } } } foreach my $parent_class_name ( @{ $class->is } ) { next unless ($involved_class_names{$parent_class_name}); my $umlet_relation = UR::Object::Umlet::Relation->get( diagram_name => $diagram->name, from_entity_name => $class->class_name, to_entity_name => $parent_class_name, ); unless ($umlet_relation) { $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<<-', from_entity_name => $class->class_name, to_entity_name => $parent_class_name, ); $umlet_relation->connect_entities(); } } } $diagram->save_to_file($params->{'file'}); 1; } sub _get_related_classes_via_properties { my($self, %params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; # Make sure the named classes are loaded foreach ( @{ $params{'names'} } ) { eval { $_->class }; } # Get everything linked to the named things my @related_names = grep { eval { $_->class } } #grep { $_ } map { $_->data_type } map { UR::Object::Property->get(class_name => $_ ) } @{ $params{'names'}}; push @related_names, grep { eval { $_->class } } #grep { $_ } map { $_->class_name } map { UR::Object::Property->get(data_type => $_ ) } @{ $params{'names'}}; return unless @related_names; my @objs = map { UR::Object::Type->get(class_name => $_) } @related_names; #my @related_names = grep { $_ } map { $_->$related_param } $related_class->get($item_param => $params{'names'}); #push @related_names, grep { $_ } map { $_->$item_param } $related_class->get($related_param => $params{'names'}); #return unless @related_names; # # my @objs = $item_class->get($item_param => \@related_names); unless ($self->include_ur_object) { # Prune out UR::Object and UR::Entity @objs = grep { $_->class_name ne 'UR::Object' and $_->class_name ne 'UR::Entity' } @objs; } # make a recursive call to get the related objects by name return ( @objs, $self->_get_related_classes_via_properties( %params, names => \@related_names, depth => --$params{'depth'}) ); } sub _get_related_classes_via_inheritance { my($self,%params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; my @related_class_names; foreach my $class_name ( @{ $params{'names'} } ) { # get the class loaded eval { $class_name->class }; if ($@) { $self->warning_message("Problem loading class $class_name: $@"); next; } # Get this class' parents #push @related_class_names, $class_name->parent_classes; push @related_class_names, @{ $class_name->__meta__->is }; } my @objs = map { $_->__meta__ } @related_class_names; unless ($self->include_ur_object) { # Prune out UR::Object and UR::Entity @objs = grep { $_->class_name ne 'UR::Object' and $_->class_name ne 'UR::Entity' } @objs; } # make a recursive call to get their parents return ( @objs, $self->_get_related_classes_via_inheritance( %params, names => \@related_class_names, depth => --$params{'depth'}, ) ); } 1; TabCompletionSpec.pm000444023532023421 550012121654173 23321 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::TabCompletionSpec; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ classname => { is => 'Text', shell_args_position => 1, doc => 'The base class to use as trunk of command tree, e.g. UR::Namespace::Command', }, output => { is => 'Text', is_optional => 1, doc => 'Override output location of the opts spec file.', }, ] ); sub help_brief { "Creates a .opts file beside class/module passed as argument, e.g. UR::Namespace::Command."; } sub create { my $class = shift; my $bx = $class->define_boolexpr(@_); if($bx->specifies_value_for('classname') and !$bx->specifies_value_for('namespace_name')) { my $classname = $bx->value_for('classname'); my($namespace) = ($classname =~ m/^(\w+)::/); $bx = $bx->add_filter(namespace_name => $namespace) if $namespace; } return $class->SUPER::create($bx); } sub is_sub_command_delegator { 0; } sub execute { my $self = shift; my $class = $self->classname; eval { require Getopt::Complete; require Getopt::Complete::Cache; }; if ($@) { die "Errors using Getopt::Complete. Do you have Getopt::Complete installed? If not try 'cpanm Getopt::Complete'"; } eval "use above '$class';"; if ($@) { $self->error_message("Unable to use above $class.\n$@"); return; } (my $module_path) = Getopt::Complete::Cache->module_and_cache_paths_for_package($class, 1); my $cache_path = $module_path . ".opts"; if (-s $cache_path) { rename($cache_path, "$cache_path.bak"); } unless ($self->output) { $self->output($cache_path); } $self->status_message("Generating " . $self->output . " file for $class."); $self->status_message("This may take some time and may generate harmless warnings..."); my $fh; $fh = IO::File->new('>' . $self->output) || die "Cannot create file at " . $self->output . "\n"; if ($fh) { my $src = Data::Dumper::Dumper($class->resolve_option_completion_spec()); $src =~ s/^\$VAR1/\$$class\:\:OPTS_SPEC/; $fh->print($src); } if (-s $cache_path) { $self->status_message("\nOPTS_SPEC file created at $cache_path"); unlink("$cache_path.bak"); } else { if (-s "$cache_path.bak") { $self->error_message("$cache_path is 0 bytes, reverting to previous"); rename("$cache_path.bak", $cache_path); } else { $self->error_message("$cache_path is 0 bytes and no backup exists, removing file"); unlink($cache_path); } } } 1; SchemaDiagram.pm000444023532023421 2426012121654174 22460 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::SchemaDiagram; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ data_source => {type => 'String', doc => 'Which datasource to use', is_optional => 1}, depth => { type => 'Integer', doc => 'Max distance of related tables to include. Default is 1. 0 means show only the named tables, -1 means to include everything', is_optional => 1}, file => { type => 'String', doc => 'Pathname of the Umlet (.uxf) file' }, show_columns => { type => 'Boolean', is_optional => 1, default => 1, doc => 'Include column names in the diagram' }, initial_name => { is_many => 1, is_optional => 1, shell_args_position => 1 } ], ); sub sub_command_sort_position { 3 }; sub help_brief { "Update an Umlet diagram based on the current schema" } sub help_detail { return < 1000; # FIXME This execute() and the one from ur update class-diagram should be combined since they share # most of the code sub execute { my $self = shift; my $params = shift; #$DB::single = 1; my $namespace = $self->namespace_name; eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } # FIXME this is a workaround for a bug. If you try to get Table objects filtered by namespace, # you have to have already instantiated the namespace's data source objects into the object cache # first map { $_->_singleton_object } $namespace->get_data_sources; my @initial_name_list; if ($params->{'depth'} == -1) { # They wanted them all... Ignore whatever is on the command line @initial_name_list = map { $_->table_name} UR::DataSource::RDBMS::Table->get(namespace => $namespace); } else { @initial_name_list = $self->initial_name; } my $diagram; if (-f $params->{'file'}) { $params->{'depth'} = 0 unless (exists $params->{'depth'}); # Default is just update what's there $diagram = UR::Object::Umlet::Diagram->create_from_file($params->{'file'}); push @initial_name_list, map { $_->subject_id } UR::Object::Umlet::Class->get(diagram_name => $diagram->name); } else { $params->{'depth'} = 1 unless exists($params->{'depth'}); $diagram = UR::Object::Umlet::Diagram->create(name => $params->{'file'}); } # FIXME this can get removed when attribute defaults work correctly unless (exists $params->{'show_attributes'}) { $self->show_columns(1); } my @involved_tables = map { UR::DataSource::RDBMS::Table->get(table_name => $_, namespace => $namespace) } @initial_name_list; #foreach my $table_name ( @initial_name_list ) { # # FIXME namespace dosen't work here either # push @involved_tables, UR::DataSource::RDBMS::Table->get(namespace => $namespace, table_name => $table_name); #} #$DB::single = 1; push @involved_tables ,$self->_get_related_items( names => \@initial_name_list, depth => $params->{'depth'}, namespace => $namespace, item_class => 'UR::DataSource::RDBMS::Table', item_param => 'table_name', related_class => 'UR::DataSource::RDBMS::FkConstraint', related_param => 'r_table_name', ); my %involved_table_names = map { $_->table_name => 1 } @involved_tables; # Figure out the initial placement # The initial placement, and how much to move over for the next box my($x_coord, $y_coord, $x_inc, $y_inc) = (20,20,40,40); my @objs = sort { $b->y <=> $a->y or $b->x <=> $a->x } UR::Object::Umlet::Class->get(); if (@objs) { my $maxobj = $objs[0]; $x_coord = $maxobj->x + $maxobj->width + $x_inc; $y_coord = $maxobj->y + $maxobj->height + $y_inc; } # First, place all the tables' boxes my @all_boxes = UR::Object::Umlet::Class->get( diagram_name => $diagram->name ); foreach my $table ( @involved_tables ) { my $umlet_table = UR::Object::Umlet::Class->get(diagram_name => $diagram->name, subject_id => $table->table_name); my $created = 0; unless ($umlet_table) { $created = 1; $umlet_table = UR::Object::Umlet::Class->create( diagram_name => $diagram->name, subject_id => $table->table_name, label => $table->table_name, x => $x_coord, y => $y_coord, ); if ($self->show_columns) { my $attributes = $umlet_table->attributes || []; my %attributes_already_in_diagram = map { $_->{'name'} => 1 } @{ $attributes }; my %pk_properties = map { $_ => 1 } $table->primary_key_constraint_column_names; my $line_count = scalar @$attributes; foreach my $column_name ( $table->column_names ) { next if $attributes_already_in_diagram{$column_name}; $line_count++; my $column = UR::DataSource::RDBMS::TableColumn->get(table_name => $table->table_name, column_name => $column_name, namespace => $namespace); push @$attributes, { is_id => $pk_properties{$column_name} ? '+' : ' ', name => $column_name, type => $column->data_type, line => $line_count, }; } $umlet_table->attributes($attributes); } # Make sure this box dosen't overlap other boxes while(my $overlapped = $umlet_table->is_overlapping(@all_boxes) ) { if ($umlet_table->x > MAX_X_AUTO_POSITION) { $umlet_table->x(20); $umlet_table->y( $umlet_table->y + $y_inc); } else { $umlet_table->x( $overlapped->x + $overlapped->width + $x_inc ); } } push @all_boxes, $umlet_table; } if ($created) { $x_coord = $umlet_table->x + $umlet_table->width + $x_inc; if ($x_coord > MAX_X_AUTO_POSITION) { $x_coord = 20; $y_coord += $y_inc; } } } # Next, connect the tables together foreach my $table ( @involved_tables ) { foreach my $fk ( UR::DataSource::RDBMS::FkConstraint->get(table_name => $table->table_name, namespace => $namespace) ) { next unless ($involved_table_names{$fk->r_table_name}); my $umlet_relation = UR::Object::Umlet::Relation->get( #diagram_name => $diagram->name, from_entity_name => $fk->table_name, to_entity_name => $fk->r_table_name, ); unless ($umlet_relation) { my @fk_column_names = $fk->column_name_map(); my $label = join("\n", map { $_->[0] . " -> " . $_->[1] } @fk_column_names); $umlet_relation = UR::Object::Umlet::Relation->create( diagram_name => $diagram->name, relation_type => '<-', label => $label, from_entity_name => $fk->table_name, to_entity_name => $fk->r_table_name, ); $umlet_relation->connect_entities(); } } } $diagram->save_to_file($params->{'file'}); 1; } sub _get_related_items { my($self, %params) = @_; return unless (@{$params{'names'}}); return unless $params{'depth'}; my $item_class = $params{'item_class'}; my $item_param = $params{'item_param'}; my $related_class = $params{'related_class'}; my $related_param = $params{'related_param'}; # Get everything linked to the named things my @related_names = map { $_->$related_param } $related_class->get($item_param => $params{'names'}, namespace => $params{'namespace'}); push @related_names, map { $_->$item_param } $related_class->get($related_param => $params{'names'}, namespace => $params{'namespace'}); return unless @related_names; my @objs = $item_class->get($item_param => \@related_names, namespace => $params{'namespace'}); # make a recursive call to get the related objects by name return ( @objs, $self->_get_related_items( %params, names => \@related_names, depth => --$params{'depth'}) ); } 1; ClassesFromDb.pm000444023532023421 16125312121654174 22506 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::ClassesFromDb; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use Text::Diff; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ data_source => { is => 'List', is_optional => 1, doc => 'Limit updates to these data sources' }, force_check_all_tables => { is => 'Boolean', is_optional => 1, doc => 'By default we only look at tables with a new DDL time for changed database schema information. This explicitly (slowly) checks each table against our cache.' }, force_rewrite_all_classes => { is => 'Boolean', is_optional => 1, doc => 'By default we only rewrite classes where there are database changes. Set this flag to rewrite all classes even where there are no schema changes.' }, table_name => { is => 'List', is_optional => 1, doc => 'Update the specified table.' }, class_name => { is => 'List', is_optional => 1, doc => 'Update only the specified classes.' }, ], ); sub sub_command_sort_position { 2 }; sub help_brief { "Update class definitions (and data dictionary cache) to reflect changes in the database schema." } sub help_detail { return <SUPER::create(%params); return unless $obj; $obj->{'_override_no_commit_for_filesystem_items'} = $override if $override; return $obj; } our @dd_classes = ( 'UR::DataSource::RDBMS::Table', 'UR::DataSource::RDBMS::TableColumn', 'UR::DataSource::RDBMS::FkConstraint', 'UR::DataSource::RDBMS::Table::Ghost', 'UR::DataSource::RDBMS::TableColumn::Ghost', 'UR::DataSource::RDBMS::FkConstraint::Ghost', ); sub execute { my $self = shift; # # Command parameter checking # my $force_check_all_tables = $self->force_check_all_tables; my $force_rewrite_all_classes = $self->force_rewrite_all_classes; my $namespace = $self->namespace_name; $self->status_message("Updating namespace: $namespace\n"); my @namespace_data_sources = $namespace->get_data_sources; my $specified_table_name_arrayref = $self->table_name; my $specified_data_source_arrayref = $self->data_source; my $specified_class_name_arrayref = $self->class_name; my @data_dictionary_objects; if ($specified_class_name_arrayref or $specified_table_name_arrayref) { my $ds_table_list; if ($specified_class_name_arrayref) { $ds_table_list = [ map { [$_->data_source, $_->table_name] } map { $_->__meta__ } @$specified_class_name_arrayref ]; } else { $ds_table_list = [ map { [$_->data_source, $_->table_name] } UR::DataSource::RDBMS::Table->get(table_name => $specified_table_name_arrayref) ]; for my $item (@$ds_table_list) { UR::Object::Type->get(data_source => $item->[0], table_name => $item->[1]); } } for my $item (@$ds_table_list) { my ($data_source, $table_name) = @$item; $self->_update_database_metadata_objects_for_schema_changes( data_source => $data_source, force_check_all_tables => $force_check_all_tables, table_name => $table_name, ); for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) { push @data_dictionary_objects, $dd_class->get(data_source_obj => $data_source, table_name => $table_name); } } } else { # Do the update by data source, all or whatever is specified. # # Determine which data sources to update from. # By default, we do all datasources owned by the namespace. # my @target_data_sources; if ($specified_data_source_arrayref) { @target_data_sources = (); my %data_source_is_specified = map { $_ => 1 } @$specified_data_source_arrayref; for my $ds (@namespace_data_sources) { if ($data_source_is_specified{$ds->id}) { push @target_data_sources, $ds; delete $data_source_is_specified{$ds->id}; } } #delete @data_source_is_specified{@namespace_data_sources}; if (my @unknown = keys %data_source_is_specified) { $self->error_message( "Unknown data source(s) for namespace $namespace: @unknown!\n" . "Select from:\n" . join("\n",map { $_->id } @namespace_data_sources) . "\n" ); return; } } else { # Don't update the Meta datasource, unless they specificly asked for it @target_data_sources = grep { $_->id !~ /::Meta$/ } @namespace_data_sources; } # Some data sources can't handle the magic required for automatic class updating... @target_data_sources = grep { $_->can('get_table_names') } @target_data_sources; $self->status_message("Found data sources: " . join(", " , map { /${namespace}::DataSource::(.*)$/; $1 || $_ } map { $_->id } @target_data_sources ) ); # # A copy of the database metadata is in the ::Meta sqlite datasource. # Get updates to it first. # ##$DB::single = 1; for my $data_source (@target_data_sources) { # ensure the class has been lazy-loaded until UNIVERSAL::can is smarter... $data_source->class; $self->status_message("Checking " . $data_source->id . " for schema changes ..."); my $success = $self->_update_database_metadata_objects_for_schema_changes( data_source => $data_source, force_check_all_tables => $force_check_all_tables, ); unless ($success) { return; } } # # Summarize the database changes by table. We'll create/update/delete the class which goes with that table. # ##$DB::single = 1; my $cx = UR::Context->current; for my $dd_class (qw/UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::FkConstraint UR::DataSource::RDBMS::TableColumn/) { push @data_dictionary_objects, grep { $force_rewrite_all_classes ? 1 : $_->__changes__ or exists($_->{'db_saved_uncommitted'}) } $cx->all_objects_loaded($dd_class); my $ghost_class = $dd_class . "::Ghost"; push @data_dictionary_objects, $cx->all_objects_loaded($ghost_class); } } # The @data_dictionary_objects array has all dd meta which should be used to rewrite classes. my %changed_tables; for my $obj ( @data_dictionary_objects ) { my $table; if ($obj->can("get_table")) { $table = $obj->get_table; unless ($table) { Carp::confess("No table object for $obj" . $obj->id); } } elsif ($obj->isa("UR::DataSource::RDBMS::Table") or $obj->isa("UR::DataSource::RDBMS::Table::Ghost")) { $table = $obj } # we may find no table if it was dropped, and this is one of its old cols/constraints next unless $table; $changed_tables{$table->id} = 1; } # Some ill-behaved modules might set no_commit to true at compile time. # Reset it back to whatever it is now after going through the namespace's modules # Note that when we have class info in the metadata DB, this probably won't be # necessary anymore since we won't have to actually load up the .pm files to # discover classes in the namespace my $remembered_no_commit_setting = UR::DBI->no_commit(); my $remembered_dummy_ids_setting = UR::DataSource->use_dummy_autogenerated_ids(); # # Update the classes based-on changes to the database schemas # ##$DB::single = 1; if (@data_dictionary_objects) { $self->status_message("Found " . keys(%changed_tables) . " tables with changes.") unless $force_rewrite_all_classes; $self->status_message("Resolving corresponding class changes..."); my $success = $self->_update_class_metadata_objects_to_match_database_metadata_changes( data_dictionary_objects => \@data_dictionary_objects ); unless ($success) { return; } } else { $self->status_message("No data schema changes."); } UR::DBI->no_commit($remembered_no_commit_setting); UR::DataSource->use_dummy_autogenerated_ids($remembered_dummy_ids_setting); # # The namespace module may have special rules for creating classes from regular (non-schema) data. # At this point we allow the namespace to adjust the class tree as it chooses. # $namespace->class; if ( $namespace->can("_update_classes_from_data_sources") and not $specified_table_name_arrayref and not $specified_class_name_arrayref and not $specified_data_source_arrayref ) { $self->status_message("Checking for custom changes for the $namespace namespace..."); $namespace->_update_classes_from_data_sources(); } $self->status_message("Saving metadata changes..."); my $sync_success = UR::Context->_sync_databases(); unless ($sync_success) { ##$DB::single = 1; $self->error_message("Metadata sync_database failed"); UR::Context->_rollback_databases(); return; } # # Re-write the class headers for changed classes. # Output a summary report of what has been changed. # This block of logic shold be part of saving class data. # Right now, it's done with a _load() override, no data_source, and this block of code. :( # ##$DB::single = 1; my $cx = UR::Context->current; my @changed_class_meta_objects; my %changed_classes; my $module_update_success = eval { for my $meta_class (qw/ UR::Object::Type UR::Object::Property /) { push @changed_class_meta_objects, grep { $_->__changes__ } $cx->all_objects_loaded($meta_class); my $ghost_class = $meta_class . "::Ghost"; push @changed_class_meta_objects, $cx->all_objects_loaded($ghost_class); } for my $obj ( @changed_class_meta_objects ) { my $class_name = $obj->class_name; next unless $class_name; #if $obj is a ghost, class_name might return undef? $changed_classes{$class_name} = 1; } unless (@changed_class_meta_objects) { $self->status_message("No class changes."); } my $changed_class_count = scalar(keys %changed_classes); my $subj = $changed_class_count == 1 ? "class" : "classes"; $self->status_message("Resolved changes for $changed_class_count $subj"); $self->status_message("Updating the filesystem..."); my $success = $self->_sync_filesystem( changed_class_names => [sort keys %changed_classes], ); return $success; }; if ($@) { $self->error_message("Error updating the filesystem: $@"); return; } elsif (!$module_update_success) { $self->status_message("Error updating filesystem!"); return; } $self->status_message("Filesystem update complete."); # # This commit actually records the data dictionary changes in the ::Meta datasource sqlite database. # $self->status_message("Committing changes to data sources..."); unless (UR::Context->_commit_databases()) { ##$DB::single = 1; $self->error_message("Metadata commit failed"); return; } # # The logic below is only necessary if this process is run as part of some larger process. # Right now that includes the automated test for this module. # After classes have been updated they won't function properly. # Ungenerate and re-generate each of the classes we touched, so that it functions according to its new spec. # $self->status_message("Cleaning up."); my $success = 1; for my $class_name (sort keys %changed_classes) { my $class_obj = UR::Object::Type->get($class_name); next unless $class_obj; $class_obj->ungenerate; Carp::confess("class $class_name didn't ungenerate properly") if $class_obj->generated; unless (eval { $class_obj->generate } ) { $self->warning_message("Class $class_name didn't re-generate properly: $@"); $success = 0; } } unless ($success) { $self->status_message("Errors occurred re-generating some classes after update."); return; } # # Done # $self->status_message("Update complete."); return 1; } # # The execute() method above is broken into three parts: # ->_update_database_metadata_objects_for_schema_changes() # ->_update_class_metadata_objects_to_match_database_metadata_changes() # ->_sync_filesystem() # sub _update_database_metadata_objects_for_schema_changes { my ($self, %params) = @_; my $data_source = delete $params{data_source}; my $force_check_all_tables = delete $params{force_check_all_tables}; my $table_name = delete $params{table_name}; die "unknown params " . Dumper(\%params) if keys %params; #$data_source = $data_source->class; my @changed; my $last_ddl_time_for_table_name = {}; if ($data_source->can("get_table_last_ddl_times_by_table_name") and !$force_check_all_tables) { # the driver implements a way to get the last DDL time $last_ddl_time_for_table_name = $data_source->get_table_last_ddl_times_by_table_name; } # from the cache of known tables my @previous_table_names = $data_source->get_table_names; my %previous_table_names = map { $_ => 1 } @previous_table_names; # from the database now my @current_table_names = $data_source->_get_table_names_from_data_dictionary(); my %current_table_names = map { s/"|'//g; $_ => $_ } @current_table_names; my %all_table_names = (%current_table_names, %previous_table_names); if($table_name) { %all_table_names = ($table_name => 1) } my $new_object_revision = $UR::Context::current->now(); # handle tables which are new/updated by updating the class my (@create,@delete,@update); my $pattern = '%-42s'; my ($dsn) = ($data_source->id =~ /^.*::DataSource::(.*?)$/); for my $table_name (keys %all_table_names) { my $last_actual_ddl_time = $last_ddl_time_for_table_name->{$table_name}; my $table_object; my $last_recorded_ddl_time; my $last_object_revision; # UR always keeps table names stored in upper-case. Some databases (mysql) # are case sensitive when querying the data dictionary my $db_table_name = $current_table_names{$table_name}; eval { #($table_object) = $data_source->get_tables(table_name => $table_name); # Using the above doesn't account for a table switching databases, which happens. # Once the data source is _part_ of the id we'll just have a delete/add, but for now it's an update. $table_object = UR::DataSource::RDBMS::Table->get(data_source => $data_source->id, table_name => $table_name); }; if ($current_table_names{$table_name} and not $table_object) { # new table push @create, $table_name; $self->status_message( sprintf( "A $pattern Schema changes " . ($last_actual_ddl_time ? "on $last_actual_ddl_time" : ""), $dsn . " " . $table_name ) ); my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name); next unless $table_object; $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name}); } elsif ($current_table_names{$table_name} and $table_object) { # retained table # either we know it changed, or we can't know, so update it anyway if (! exists $last_ddl_time_for_table_name->{$table_name} or ! defined $table_object->last_ddl_time or $last_ddl_time_for_table_name->{$table_name} gt $table_object->last_ddl_time ) { my $last_update = $table_object->last_ddl_time || $table_object->last_object_revision; my $this_update = $last_ddl_time_for_table_name->{$table_name} || ""; my $table_object = $data_source->refresh_database_metadata_for_table_name($db_table_name); unless ($table_object) { ##$DB::single = 1; print; } my @changes = # grep { not ($_->properties == 1 and ($_->properties)[0] eq "last_object_revision") } $table_object->__changes__; if (@changes) { $self->status_message( sprintf("U $pattern Last updated on $last_update. Newer schema changes on $this_update." , $dsn . " " . $table_name ) ); push @update, $table_name; } $table_object->last_ddl_time($last_ddl_time_for_table_name->{$table_name}); } } elsif ($table_object and not $current_table_names{$table_name}) { # deleted table push @delete, $table_name; $self->status_message( sprintf( "D $pattern Last updated on %s. Table dropped.", $dsn . " " . $table_name, $last_object_revision || "" ) ); my $table_object = UR::DataSource::RDBMS::Table->get( data_source => $data_source->id, table_name => $table_name, ); $table_object->delete; } else { Carp::confess("Unable to categorize table $table_name as new/old/deleted?!"); } } return 1; } # Keep a cache of class meta objects so we don't have to keep asking the # object system to do it for us. This should be a speed optimization because # the asking eventually filters down to calling get_material_classes() on the # namespace which can be extremely slow. If it's not in the cache, defer to # asking the data source sub _get_class_meta_for_table_name { my($self,%param) = @_; my $data_source = $param{'data_source'}; my $data_source_name = $data_source->get_name(); my $table_name = $param{'table_name'}; my ($obj) = grep { not $_->isa("UR::Object::Ghost") } UR::Object::Type->is_loaded( data_source_id => $data_source, table_name => $table_name ); return $obj if $obj; unless ($self->{'_class_meta_cache'}{$data_source_name}) { my @classes = grep { not $_->class_name->isa('UR::Object::Ghost') } UR::Object::Type->get(data_source_id => $data_source); for my $class (@classes) { my $table_name = $class->table_name; next unless $table_name; $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name} = $class; } } $obj = $self->{'_class_meta_cache'}->{$data_source_name}->{$table_name}; return $obj if $obj; return; } sub _update_class_metadata_objects_to_match_database_metadata_changes { my ($self, %params) = @_; my $data_dictionary_objects = delete $params{data_dictionary_objects}; if (%params) { $self->error_message("Unknown params!"); return; } # # INITIALIZATION AND SANITY CHECKING # my $namespace = $self->namespace_name; =cut $self->status_message("Using filesystem classes for namespace \"$namespace\" (this may be slow)"); my @material_classes = $namespace->get_material_classes; $self->status_message("Verifying class/table relationships..."); my %table_ids_used; for my $class (sort { $a->class_name cmp $b->class_name } @material_classes) { my $table_name = $class->table_name; next unless $table_name; my $class_name = $class->class_name; if (my $prev_class_name = $table_ids_used{$table_name}) { $self->error_message( sprintf( "C %-40s uses table %-32s, but so does %-40s" . "\n", $class_name, $table_name, $prev_class_name ) ); return; } my $data_source = $class->data_source; my $table = UR::DataSource::RDBMS::Table->get(data_source => $data_source, table_name => $table_name) || UR::DataSource::RDBMS::Table::Ghost->get(data_source => $data_source, table_name => $table_name); unless ($table) { $self->error_message( sprintf( "C %-32s %-32s is referenced by class %-40s but cannot be found!?" . "\n", $data_source, $table_name, $class_name ) ); return; } $table_ids_used{$table_name} = $class; } =cut $self->status_message("Updating classes..."); my %dd_changes_by_class = ( 'UR::DataSource::RDBMS::Table' => [], 'UR::DataSource::RDBMS::TableColumn' => [], 'UR::DataSource::RDBMS::FkConstraint' => [], 'UR::DataSource::RDBMS::Table::Ghost' => [], 'UR::DataSource::RDBMS::TableColumn::Ghost' => [], 'UR::DataSource::RDBMS::FkConstraint::Ghost' => [], ); for my $changed_obj (@$data_dictionary_objects) { my $changed_class = $changed_obj->class; my $bucket = $dd_changes_by_class{$changed_class}; push @$bucket, $changed_obj; } my $sorter = sub($$) { no warnings 'uninitialized'; $_[0]->table_name cmp $_[1]->table_name || $_[0]->id cmp $_[1]->id }; # FKs are special, in that they might change names, but we use the name as the "id". # This should change, really, but until it does we need to identify them by their "content", # # DELETIONS # # DELETED FK CONSTRAINTS # Just detach the object reference meta-data from the constraint. # We only actually delete references when their properties all go away, # which can happen when the columns go away (through table deletion or alteration). # It can also happen when one of the involved classes is deleted, which never happens # automatically. for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint::Ghost'} }) { unless ($fk->table_name) { $self->status_message(sprintf("~ No table name for deleted foreign key constraint %-32s\n", $fk->id)); next; } my $table = $fk->get_table; # FIXME should this use $data_source->get_class_meta_for_table($table) instead? my $class = UR::Object::Type->get( data_source_id => $table->data_source, table_name => $table->table_name, ) || UR::Object::Type::Ghost->get( data_source_id => $table->data_source, table_name => $table->table_name, ); unless ($class) { ##$DB::single = 1; $self->status_message(sprintf("~ No class found for deleted foreign key constraint %-32s %-32s\n",$table->table_name, $fk->id)); next; } my $class_name = $class->class_name; my $property = UR::Object::Property->get(class_name => $class_name, constraint_name => $fk->fk_constraint_name); unless ($property) { $self->status_message(sprintf("~ No property found for deleted foreign key constraint %-32s %-32s class $class_name\n", $table->table_name, $fk->fk_constraint_name)); next; } $property->delete; } # DELETED UNIQUE CONSTRAINTS # DELETED PK CONSTRAINTS # We do nothing here, because we don't track these as individual DD objects, just values on the table object. # If a table changes constraints, that is handled below after table/column add/update. # If a table is dropped entirely, we leave all pk/unique constraints in place, # since, if the class is not manually deleted by the developer, it should continue # to function as it did before. # DELETED COLUMNS my @saved_removed_column_messages; # Delete them now, but report about them later in the 'Updating class properties' section for my $column (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::TableColumn::Ghost"} }) { my $table = $column->get_table; unless ($table) { $self->status_message(sprintf("~ No table found for deleted column %-32s\n", $column->id)); next; } my $column_name = $column->column_name; # FIXME should this use $data_source->get_class_meta_for_table($table) instead? my $class = UR::Object::Type->get( data_source_id => $table->data_source, table_name => $table->table_name, ); unless ($class) { $self->status_message(sprintf("~ No class found for deleted column %-32s %-32s\n", $table->table_name, $column_name)); next; } my $class_name = $class->class_name; my ($property) = $class->direct_property_metas( column_name => $column_name ); unless ($property) { $self->status_message(sprintf("~ No property found for deleted column %-32s %-32s\n",$table->table_name, $column_name)); next; } unless ($table->isa("UR::DataSource::RDBMS::Table::Ghost")) { push(@saved_removed_column_messages, sprintf("D %-40s property %-16s for removed column %s.%s\n", $class->class_name, $property->property_name, $column->table_name, $column->column_name, ) ); } $property->delete; unless ($property->isa("UR::DeletedRef")) { Carp::confess("Error deleting property " . $property->id); } } # DELETED TABLES my %classes_with_deleted_tables; for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table::Ghost"} }) { # Though we create classes for tables, we don't immediately delete them, just deflate them. my $table_name = $table->table_name; unless ($table_name) { $self->status_message("~ No table_name for deleted table object ".$table->id); next; } if (not defined UR::Context->_get_committed_property_value($table,'table_name')) { print Data::Dumper::Dumper($table); ##$DB::single = 1; } # FIXME should this use $data_source->get_class_meta_for_table($table) instead? my $committed_data_source_id = UR::Context->_get_committed_property_value($table,'data_source'); my $committed_table_name = UR::Context->_get_committed_property_value($table,'table_name'); my $class = UR::Object::Type->get( data_source_id => $committed_data_source_id, table_name => $committed_table_name, ); unless ($class) { $self->status_message(sprintf("~ No class found for deleted table %-32s" . "\n",$table->id)); next; } $classes_with_deleted_tables{$table_name} = $class; $class->data_source(undef); $class->table_name(undef); } # next deleted table for my $table_name (keys %classes_with_deleted_tables) { my $class = $classes_with_deleted_tables{$table_name}; my $class_name = $class->class_name; my %ancestory = map { $_ => 1 } $class->inheritance; my @ancestors_with_tables = grep { $a = UR::Object::Type->get(class_name => $_) || UR::Object::Type::Ghost->get(class_name => $_); $a && $a->table_name; } sort keys %ancestory; if (@ancestors_with_tables) { $self->status_message( sprintf("U %-40s class is now detached from deleted table %-32s. It still inherits from classes with persistent storage." . "\n",$class_name,$table_name) ); } else { $self->status_message( sprintf("D %-40s class deleted for deleted table %s" . "\n",$class_name,$table_name) ); } } # next deleted table # This is the data structure used by _get_class_meta_for_table_name # There's a bad interaction with software transactions that can lead # to this cache containing deleted class objects if the caller holds # on to a reference to this command object and repetedly calls execute() # but rolls back transactions between those calls. $self->{'_class_meta_cache'} = {}; ##$DB::single = 1; # # EXISTING DD OBJECTS # # TABLE for my $table (sort $sorter @{ $dd_changes_by_class{"UR::DataSource::RDBMS::Table"} }) { my $table_name = $table->table_name; my $data_source = $table->data_source; my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table_name); if ($class) { # update if ($class->data_source ne $table->data_source) { $class->data_source($table->data_source); } my $class_name = $class->class_name; no warnings; if ($table->remarks ne UR::Context->_get_committed_property_value($table,'remarks')) { $class->doc($table->remarks); } if ($table->data_source ne UR::Context->_get_committed_property_value($table,'data_source')) { $class->data_source($table->data_source); } if ($class->__changes__) { $self->status_message( sprintf("U %-40s class uses %s %s %s" . "\n", $class_name, $table->data_source->get_name, lc($table->table_type), $table_name) ); } } else { # create my $data_source = $table->data_source; my $class_name = $data_source->resolve_class_name_for_table_name($table_name,$table->table_type); unless ($class_name) { Carp::confess( "Failed to resolve a class name for new table " . $table_name ); } # if the original table_name was empty (ie. not backed by a table), and the # new one actually has a table, then this is just another schema change and # not an error. Set the table_name attribute and go on... my $class = UR::Object::Type->get(class_name => $class_name); my $prev_table_name = $class->table_name if ($class); if ($class && $prev_table_name) { Carp::confess( "Class $class_name already exists for table '$prev_table_name'." . " Cannot generate class for $table_name." ); } $self->status_message( sprintf("A %-40s class uses %s %s %s" . "\n", $class_name, $table->data_source->get_name, lc($table->table_type), $table_name) ); if ($class) { $class->doc($table->remarks ? $table->remarks: undef); $class->data_source($data_source); $class->table_name($table_name); $class->er_role($table->er_type); } else { $class = UR::Object::Type->create( class_name => $class_name, doc => ($table->remarks ? $table->remarks: undef), data_source_id => $data_source, table_name => $table_name, er_role => $table->er_type, # generate => 0, ); unless ($class) { Carp::confess( "Failed to create class $class_name for new table " . $table_name . ". " . UR::Object::Type->error_message ); } } #unless ($class->class_name->isa('UR::Entity')) { # my $inheritance = UR::Object::Inheritance->create( # class_name => $class->class_name, # parent_class_name => "UR::Entity", # inheritance_priority => 0, # ); # Carp::confess("Failed to generate inheritance link!?") unless $inheritance; #} } } # next table $self->status_message("Updating direct class properties...\n"); $self->status_message($_) foreach @saved_removed_column_messages; # COLUMN my @column_property_translations = ( ['data_type' => 'data_type'], ['data_length' => 'data_length'], ['nullable' => 'is_optional', sub { (defined($_[0]) and ($_[0] eq "Y")) ? 1 : 0 } ], ['remarks' => 'doc'], ); for my $column (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::TableColumn'} }) { my $table = $column->get_table; my $column_name = $column->column_name; my $data_source = $table->data_source; my($ur_data_type,$default_length) = @{ $data_source->ur_data_type_for_data_source_data_type($column->data_type) }; my $ur_data_length = defined($column->data_length) ? $column->data_length : $default_length; #my $class = UR::Object::Type->get( # data_source => $table->data_source, # table_name => $table->table_name, #); #my $class = $data_source->get_class_meta_for_table($table); my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table->table_name); unless ($class) { ##$DB::single = 1; $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table->table_name); Carp::confess("Class object missing for table " . $table->table_name) unless $class; } my $class_name = $class->class_name; my $property; foreach my $prop_object ( $class->direct_property_metas ) { if (defined $prop_object->column_name and $prop_object->column_name eq $column_name) { $property = $prop_object; last; } } # We care less whether the column is new/updated, than whether there is property metadata for it. if ($property) { # update for my $translation (@column_property_translations) { my ($column_attr, $property_attr, $conversion_sub) = @$translation; $property_attr ||= $column_attr; no warnings; if (UR::Context->_get_committed_property_value($column,$column_attr) ne $column->$column_attr) { if ($conversion_sub) { $property->$property_attr($conversion_sub->($column->$column_attr)); } else { $property->$property_attr($column->$column_attr); } } } $property->data_type($ur_data_type) if (! defined $property->data_type); # lengths for these data types are based on the number of bytes used internally in the # database. The UR-based objects will store the text version, which will always be longer, # making $obj->__errors__() complain about the length being out of bounds $property->data_length($column->is_time_data ? undef : $ur_data_length) if (! defined $property->data_length); $property->is_optional($column->nullable eq "Y" ? 1 : 0); $property->doc($column->remarks); if ($property->__changes__) { no warnings; $self->status_message( sprintf("U %-40s property %-20s for column %s.%s (%s %s)\n", $class_name, $property->property_name, $table->table_name, $column_name, $column->data_type, $column->data_length) ); } } else { # create my $property_name = $data_source->resolve_property_name_for_column_name($column->column_name); unless ($property_name) { Carp::confess( "Failed to resolve a property name for new column " . $column->column_name ); } $property = UR::Object::Property->create( class_name => $class_name, property_name => $property_name, column_name => $column_name, data_type => $ur_data_type, data_length => $ur_data_length, is_optional => $column->nullable eq "Y" ? 1 : 0, is_volatile => 0, doc => $column->remarks, is_specified_in_module_header => 1, ); no warnings; $self->status_message( sprintf("A %-40s property %-16s for column %s.%s (%s %s)\n", $class_name, $property_name, $table->table_name, $column_name, $column->data_type, $column->data_length) ); unless ($property) { Carp::confess( "Failed to create property $property_name on class $class_name. " . UR::Object::Property->error_message ); } } } # next column $self->status_message("Updating class ID properties...\n"); # PK CONSTRAINTS (loop table objects again, since the DD doesn't do individual ID objects) for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) { # created/updated/unchanged # delete and re-create these objects: they're "bridges", so no developer supplied data is presesent my $table_name = $table->table_name; my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table_name); my $class_name = $class->class_name; my @properties = UR::Object::Property->get(class_name => $class_name); unless (@properties) { $self->warning_message("no properties on class $class_name?"); ##$DB::single = 1; } my @expected_pk_cols = grep { defined } map { $_->column_name } grep { defined $_->is_id } @properties; my @pk_cols = $table->primary_key_constraint_column_names; if ("@expected_pk_cols" eq "@pk_cols") { next; } unless (@pk_cols) { # If there are no primary keys defined, then treat _all_ the columns # as primary keys. This means we don't support multiple rows in a # table containing the same data. @pk_cols = $table->column_names; } my %pk_cols; for my $pos (1 .. @pk_cols) { my $pk_col = $pk_cols[$pos-1]; my ($property) = grep { defined($_->column_name) and ($_->column_name eq $pk_col) } @properties; unless ($property) { # the column has been removed next; } $pk_cols{$property->property_name} = $pos; } # all primary key properties are non-nullable, regardless of what the DB allows for my $property (@properties) { my $name = $property->property_name; if ($pk_cols{$name}) { $property->is_optional(0); $property->is_id($pk_cols{$name}); } } } # next table (looking just for PK constraint changes) # Make another pass to make sure if a class has a property called 'id' with a column attached, # then it must be the only ID property of that class my %classes_to_check_id_properties; foreach my $thing ( qw(UR::DataSource::RDBMS::Table UR::DataSource::RDBMS::TableColumn ) ) { foreach my $item ( @{ $dd_changes_by_class{$thing} } ) { my $class_meta = $self->_get_class_meta_for_table_name(data_source => $item->data_source, table_name => $item->table_name); $classes_to_check_id_properties{$class_meta->class_name} ||= $class_meta; } } foreach my $class_name ( keys %classes_to_check_id_properties ) { my $class_meta = $classes_to_check_id_properties{$class_name}; my $property_meta = $class_meta->property_meta_for_name('id'); if ($property_meta && $property_meta->column_name && scalar($class_meta->direct_id_property_metas) > 1) { $self->warning_message("Class $class_name cannot have multiple ID properties when one concrete ID property is named 'id'. It will likely not function correctly unless it is renamed"); } unless (defined $property_meta->is_id) { $self->warning_message("Class $class_name has a property named 'id' that is not an ID property. It will likely not function correctly unless it is renamed"); } } $self->status_message("Updating class unique constraints...\n"); ##$DB::single = 1; # UNIQUE CONSTRAINT / UNIQUE INDEX -> UNIQUE GROUP (loop table objecs since we have no PK DD objects) for my $table (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::Table'} }) { # created/updated/unchanged # delete and re-create #my $class = UR::Object::Type->get( # data_source => $table->data_source, # table_name => $table->table_name, #); #my $class = $table->__meta__(); my $class = $self->_get_class_meta_for_table_name(data_source => $table->data_source, table_name => $table->table_name); my $class_name = $class->class_name; my @properties = UR::Object::Property->get(class_name => $class_name); my @uc_names = $table->unique_constraint_names; for my $uc_name (@uc_names) { eval { $class->remove_unique_constraint($uc_name) }; if ($@ =~ m/There is no constraint named/) { next; # it's OK if there's no UR metadata for this constraint yet } else { die $@; } my @uc_cols = map { ref($_) ? @$_ : $_ } $table->unique_constraint_column_names($uc_name); my @uc_property_names; for my $uc_col (@uc_cols) { my ($property) = grep { defined($_->column_name) and ($_->column_name eq $uc_col) } @properties; unless ($property) { $self->warning_message("No property found for column $uc_col for unique constraint $uc_name"); #$DB::single = 1; next; } push @uc_property_names, $property->property_name; } $class->add_unique_constraint($uc_name, @uc_property_names); } } # next table (checking separately for unique constraints) # FK CONSTRAINTS # These often change name, and as such need to be identified by their actual content. # Each constraint must match some relationship in the system, or a new one will be added. $self->status_message("Updating class relationships...\n"); my $last_class_name = ''; FK: for my $fk (sort $sorter @{ $dd_changes_by_class{'UR::DataSource::RDBMS::FkConstraint'} }) { my $table = $fk->get_table; my $data_source = $fk->data_source; my $table_name = $fk->table_name; my $r_table_name = $fk->r_table_name; my $class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $table_name); unless ($class) { $self->warning_message( sprintf("No class found for table for foreign key constraint %-32s %s" . "\n",$table_name, $fk->id) ); next; } my $r_class = $self->_get_class_meta_for_table_name(data_source => $data_source, table_name => $r_table_name); unless ($r_class) { $self->warning_message( sprintf("No class found for r_table for foreign key constraint %-32s %-32s" . "\n",$r_table_name, $fk->id) ); next; } my $class_name = $class->class_name; my $r_class_name = $r_class->class_name; # Create an object-accessor property to go with this FK # First we have to figure out a proper delegation name # which is a rather convoluted process my @column_names = $fk->column_names; my @r_column_names = $fk->r_column_names; my (@properties,@property_names,@r_properties,@r_property_names,$prefix,$suffix,$matched); foreach my $i ( 0 .. $#column_names ) { my $column_name = $column_names[$i]; my $property = UR::Object::Property->get( class_name => $class_name, column_name => $column_name, ); unless ($property) { Carp::confess("Failed to find a property for column $column_name on class $class_name"); } push @properties,$property; my $property_name = $property->property_name; push @property_names,$property_name; my $r_column_name = $r_column_names[$i]; my $r_property = UR::Object::Property->get( class_name => $r_class_name, column_name => $r_column_name, ); unless ($r_property) { Carp::cluck("Failed to find a property for column $r_column_name on class $r_class_name"); #$DB::single = 1; next FK; } push @r_properties,$r_property; my $r_property_name = $r_property->property_name; push @r_property_names,$r_property_name; if ($property_name =~ /^(.*)$r_property_name(.*)$/ or $property_name =~ /^(.*)_id$/) { $prefix = $1; $prefix =~ s/_$//g if defined $prefix; $suffix = $2; $suffix =~ s/^_//g if defined $suffix; $matched = 1; } } my @r_class_name_parts = split('::', $r_class->class_name); shift @r_class_name_parts; # drop the namespace name my $delegation_name = lc(join('_', @r_class_name_parts)); if ($matched) { $delegation_name = $delegation_name . "_" . $prefix if $prefix; $delegation_name .= ($suffix !~ /\D/ ? "" : "_") . $suffix if $suffix; } else { $delegation_name = join("_", @property_names) . "_" . $delegation_name; } # Generate a delegation name that dosen't conflict with another already in use my %property_names_used = map { $_ => 1 } $class->all_property_names; while($property_names_used{$delegation_name}) { $delegation_name =~ /^(.*?)(\d*)$/; $delegation_name = $1 . ( ($2 ? $2 : 0) + 1 ); } # FK columns may have been in an odd order. Get the reference columns in ID order. for my $i (0..$#column_names) { my $column_name = $column_names[$i]; my $property = $properties[$i]; my $property_name = $property_names[$i]; my $r_column_name = $r_column_names[$i]; my $r_property = $r_properties[$i]; my $r_property_name = $r_property_names[$i]; } # Pick a name that isn't already a property in that class PICK_A_NAME: for ( 1 ) { if (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name)) { if (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name.'_obj')) { foreach my $i ( 1 .. 10 ) { unless (UR::Object::Property->get(class_name => $class_name, property_name => $delegation_name."_$i")) { $delegation_name .= "_$i"; last PICK_A_NAME; } } $self->warning_message("Can't generate a relationship property name for $class_name table name $table_name constraint_name ",$fk->fk_constraint_name); next FK; } else { $delegation_name = $delegation_name.'_obj'; } } } unless ($class->property_meta_for_name($delegation_name)) { my $property = UR::Object::Property->create(class_name => $class_name, property_name => $delegation_name, data_type => $r_class_name, id_by => \@property_names, constraint_name => $fk->fk_constraint_name, is_delegated => 1, is_specified_in_module_header => 1, ); no warnings; $self->status_message( sprintf("A %-40s property %-16s id by %-16s (%s)\n", $class_name, $delegation_name, join(',',@property_names), $r_class_name ) ); } } # next fk constraint return 1; } sub _foreign_key_fingerprint { my($self,$fk) = @_; my $class = $self->_get_class_meta_for_table_name(data_source => $fk->data_source, table_name => $fk->table_name); return $class->class_name . ':' . join(',',sort $fk->column_names) . ':' . join(',',sort $fk->r_column_names); } sub _sync_filesystem { my $self = shift; my %params = @_; my $changed_class_names = delete $params{changed_class_names}; if (%params) { Carp::confess("Invalid params passed to _sync_filesystem: " . join(",", keys %params) . "\n"); } my $obsolete_module_directory = $self->namespace_name->get_deleted_module_directory_name; my $namespace = $self->namespace_name; my $no_commit = UR::DBI->no_commit; $no_commit = 0 if $self->{'_override_no_commit_for_filesystem_items'}; for my $class_name (@$changed_class_names) { my $status_message_this_update = ''; my $class_obj; my $prev; if ($class_obj = UR::Object::Type->get(class_name => $class_name)) { if ($class_obj->{is}[0] =~ /::Type$/ and $class_obj->{is}[0]->isa('UR::Object::Type')) { next; } if ($class_obj->{db_committed}) { $status_message_this_update .= "U " . $class_obj->module_path; } else { $status_message_this_update .= "A " . $class_obj->module_path; } $class_obj->rewrite_module_header() unless ($no_commit); # FIXME A test of automaticly making DBIx::Class modules #$class_obj->dbic_rewrite_module_header() unless ($no_commit); } elsif ($class_obj = UR::Object::Type::Ghost->get(class_name => $class_name)) { if ($class_obj->{is}[0] eq 'UR::Object::Type') { next; } $status_message_this_update = "D " . $class_obj->module_path; unless ($no_commit) { unless (-d $obsolete_module_directory) { mkdir $obsolete_module_directory; unless (-d $obsolete_module_directory) { $self->error_message("Unable to create $obsolete_module_directory for the deleted module for $class_name."); next; } } my $f = IO::File->new($class_obj->module_path); my $old_file_data = join('',$f->getlines); $f->close(); my $old_module_path = $class_obj->module_path; my $new_module_path = $old_module_path; $new_module_path =~ s/\/$namespace\//\/$namespace\/\.deleted\//; $status_message_this_update .= " (moving $old_module_path to $new_module_path)"; rename $old_module_path, $new_module_path; UR::Context::Transaction->log_change($class_obj, $class_obj->class_name, $class_obj->id, 'rewrite_module_header', Data::Dumper::Dumper({path => $new_module_path, data => $old_file_data})); } } else { Carp::confess("Failed to find regular or ghost class meta-object for class $class_name!?"); } if ($no_commit) { $status_message_this_update .= ' (ignored - no-commit)'; } $self->status_message($status_message_this_update); } return 1; } 1; RenameClass.pm000444023532023421 434712121654174 22154 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::RenameClass; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Update::RewriteClassHeader", ); # Standard methods used to output help. sub shell_args_description { "My::Old My::New"; } sub help_description { "Updates class descriptions for to correspond with database schema changes." } # Wrap execute since we take different parameters than a standard # "runs on modules in tree" rewrite command. our $old; our $new; sub execute { my $self = shift; $old = shift; $new = shift; $self->error_message("rename $old to $new not implemented"); return; } # Override "before" to do the class editing. sub before { my $self = shift; my $class_objects = shift; # By default, no classes are rewritten. # As we find classes with the $old name, in their metadata, # we add them to this list. $class_objects = []; print "finding properties which seem to refernce a class\n"; my @p = UR::Object::Property->is_loaded( property_name => ["class_name","r_class_name","parent_class_name"] ); print "found " . join("\n",map { $_->class_name . " -> " . $_->property_name } @_) . "\n"; print "checking instances of those properties\n"; my @changes; for my $p (@p) { my $class_name = $p->class_name; my $property_name = $p->property_name; my @obj = $class_name->is_loaded(); for my $obj (@obj) { if ($obj->$property_name eq $new) { Carp::confess("Name $new is already in use on $class_name, " . $obj->{id} . $property_name . "!" ); } elsif ($obj->$property_name eq $old) { print "Setting $new in place of $old on $class_name, " . $obj->{id} . $property_name . ".\n"; push @changes, [$obj,$property_name,$new]; } } } return 1; } # we implement before() but use the default call to # for_each_class_object() call in UR::Namespace::Command::rewrite 1; Pod.pm000444023532023421 1172612121654175 20521 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::Pod; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; class UR::Namespace::Command::Update::Pod { is => 'Command::V2', has => [ executable_name => { is => 'Text', shell_args_position => 1, doc => 'the name of the executable to document' }, class_name => { is => 'Text', shell_args_position => 2, doc => 'the command class which maps to the executable' }, targets => { is => 'Text', shell_args_position => 3, is_many => 1, doc => 'specific classes to document (documents all unless specified)', }, input_path => { is => 'Path', is_optional => 1, doc => 'optional location of the modules to document', }, output_path => { is => 'Text', is_optional => 1, doc => 'optional location to output .pod files', }, ], doc => "generate man-page-like POD for a commands" }; sub help_synopsis { return <<"EOS" ur update pod -i ./lib -o ./pod ur UR::Namespace::Command EOS } sub help_detail { return join("\n", 'This tool generates POD documentation for each all of the commands in a tree for a given executable.', 'This command must be run from within the namespace directory.'); } sub execute { my $self = shift; #$DB::single = 1; local $ENV{ANSI_COLORS_DISABLED} = 1; my $entry_point_bin = $self->executable_name; my $entry_point_class = $self->class_name; my @targets = $self->targets; unless (@targets) { @targets = ($entry_point_class); } local @INC = @INC; if ($self->input_path) { unshift @INC, $self->input_path; $self->status_message("using modules at " . $self->input_path); } my $errors = 0; for my $target (@targets) { eval "use $target"; if ($@) { $self->error_message("Failed to use $target: $@"); $errors++; } } return if $errors; my @commands = map( $self->get_all_subcommands($_), @targets); push @commands, @targets; if ($self->output_path) { unless (-d $self->output_path) { if (-e $self->output_path) { $self->status_message("output path is not a directory!: " . $self->output_path); } else { mkdir $self->output_path; if (-d $self->output_path) { $self->status_message("using output directory " . $self->output_path); } else { $self->status_message("error creating directory: $! for " . $self->output_path); } } } } local $Command::V1::entry_point_bin = $entry_point_bin; local $Command::V2::entry_point_bin = $entry_point_bin; local $Command::V1::entry_point_class = $entry_point_class; local $Command::V2::entry_point_class = $entry_point_class; for my $command (@commands) { my $pod; eval { $pod = $command->help_usage_command_pod(); }; if($@) { $self->warning_message('Could not generate POD for ' . $command . '. ' . $@); next; } unless($pod) { $self->warning_message('No POD generated for ' . $command); next; } my $pod_path; if (defined $self->output_path) { my $filename = $command->command_name . '.pod'; $filename =~ s/ /-/g; my $output_path = $self->output_path; $output_path =~ s|/+$||m; $pod_path = join('/', $output_path, $filename); } else { $pod_path = $command->__meta__->module_path; $pod_path =~ s/.pm/.pod/; } $self->status_message("Writing $pod_path"); my $fh; $fh = IO::File->new('>' . $pod_path) || die "Cannot create file at " . $pod_path . "\n"; print $fh $pod; close($fh); } return 1; } sub get_all_subcommands { my $self = shift; my $command = shift; my $src = "use $command"; eval $src; if ($@) { $self->error_message("Failed to load class $command: $@"); } else { my $module_name = $command; $module_name =~ s|::|/|g; $module_name .= '.pm'; $self->status_message("Loaded $command from $module_name at $INC{$module_name}\n"); } my @subcommands; eval { if ($command->can('sub_command_classes')) { @subcommands = $command->sub_command_classes; } }; if($@) { $self->warning_message("Error getting subclasses for module $command: " . $@); } return unless @subcommands and $subcommands[0]; #Sometimes sub_command_classes returns 0 instead of the empty list return map($self->get_all_subcommands($_), @subcommands), @subcommands; } 1; Doc.pm000444023532023421 2365412121654175 20507 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Updatepackage UR::Namespace::Command::Update::Doc; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; use File::Basename; use File::Path; use YAML; class UR::Namespace::Command::Update::Doc { is => 'Command::V2', has => [ executable_name => { is => 'Text', shell_args_position => 1, doc => 'the name of the executable to document' }, class_name => { is => 'Text', shell_args_position => 2, doc => 'the command class which maps to the executable' }, targets => { is => 'Text', is_optional => 1, shell_args_position => 3, is_many => 1, doc => 'specific classes to document (documents all unless specified)', }, exclude_sections => { is => 'Text', is_many => 1, is_optional => 1, doc => 'if specified, sections matching these names will be omitted', }, input_path => { is => 'Path', is_optional => 1, doc => 'optional location of the modules to document', }, restrict_to_input_path => { is => 'Boolean', default_value => 1, doc => 'when set, only modules found under the input-path will be processed', }, output_path => { is => 'Text', is_optional => 1, doc => 'optional location to output documentation files', }, output_format => { is => 'Text', default_value => 'pod', valid_values => ['pod', 'html'], doc => 'the output format to write' }, generate_index => { is => 'Boolean', default_value => 1, doc => "when true, an 'index' of all files generated is written (currently works for html only)", }, suppress_errors => { is => 'Boolean', default_value => 1, doc => 'when set, warnings about unloadable modules will not be printed', }, ], has_transient_optional => [ _writer_class => { is => 'Text', }, _index_filename => { is => 'Text', } ], doc => "generate documentation for commands" }; sub help_synopsis { return <<"EOS" ur update doc -i ./lib -o ./doc ur UR::Namespace::Command EOS } sub help_detail { return join("\n", 'This tool generates documentation for each of the commands in a tree for a given executable.', 'This command must be run from within the namespace directory.'); } sub execute { my $self = shift; die "--generate-index requires --output-dir to be specified" if $self->generate_index and !$self->output_path; # scrub any trailing / from input/output_path if ($self->output_path) { my $output_path = $self->output_path; $output_path =~ s/\/+$//m; $self->output_path($output_path); } if ($self->input_path) { my $input_path = $self->input_path; $input_path =~ s/\/+$//m; $self->input_path($input_path); } $self->_writer_class("UR::Doc::Writer::" . ucfirst($self->output_format)); die "Unable to create a writer for output format '" . $self->output_format . "'" unless($self->_writer_class->can("create")); local $ENV{ANSI_COLORS_DISABLED} = 1; my $entry_point_bin = $self->executable_name; my $entry_point_class = $self->class_name; my @targets = $self->targets; unless (@targets) { @targets = ($entry_point_class); } local @INC = @INC; if ($self->input_path) { unshift @INC, $self->input_path; $self->status_message("using modules at " . $self->input_path); } my $errors = 0; for my $target (@targets) { eval "use $target"; if ($@) { $self->error_message("Failed to use $target: $@"); $errors++; } } return if $errors; if ($self->output_path) { unless (-d $self->output_path) { if (-e $self->output_path) { $self->status_message("output path is not a directory!: " . $self->output_path); } else { File::Path::make_path($self->output_path); if (-d $self->output_path) { $self->status_message("using output directory " . $self->output_path); } else { $self->status_message("error creating directory: $! for " . $self->output_path); } } } } local $Command::entry_point_bin = $entry_point_bin; local $Command::entry_point_class = $entry_point_class; my @command_trees = map( $self->_get_command_tree($_), @targets); $self->_generate_index(@command_trees); for my $tree (@command_trees) { $self->_process_command_tree($tree); } return 1; } sub _generate_index { my ($self, @command_trees) = @_; if ($self->generate_index) { my $index = Dump({ command_tree => \@command_trees }); if ($index and $index ne '') { my $index_filename = "index.yml"; my $index_path = join("/", $self->output_path, $index_filename); if (-e $index_path) { $self->warning_message("Index generation overwriting existing file at $index_path"); } my $fh = IO::File->new($index_path, 'w'); unless ($fh) { Carp::croak("Can't open file $index_path for writing: $!"); } $fh->print($index); $fh->close(); $self->_index_filename($index_filename) if -e $index_path; } else { $self->warning_message("Unable to generate index"); } } return; } sub _generate_content { my ($self, $command) = @_; my $doc; eval { my @all_sections = $command->doc_sections; my @sections; for my $s (@all_sections) { push(@sections, $s) unless grep { $s->title =~ /$_/ } $self->exclude_sections; } my $writer = $self->_writer_class->create( sections => \@sections, title => $command->command_name, ); $doc = $writer->render; }; if($@) { $self->warning_message('Could not generate docs for ' . $command . '. ' . $@); return; } unless($doc) { $self->warning_message('No docs generated for ' . $command); return; } my $command_name = $command->command_name; my $filename = $self->_make_filename($command_name); my $dir = $self->_get_output_dir($command_name); my $doc_path = join("/", $dir, $filename); $self->status_message("Writing $doc_path"); my $fh; $fh = IO::File->new('>' . $doc_path) || die "Cannot create file at " . $doc_path . "\n"; print $fh $doc; close($fh); } sub _process_command_tree { my ($self, $tree) = @_; $self->_generate_content($tree->{command}) unless $tree->{external}; for my $subtree (@{$tree->{sub_commands}}) { $self->_process_command_tree($subtree); } } sub _make_filename { my ($self, $class_name) = @_; $class_name =~ s/ /-/g; return "$class_name." . $self->output_format; } sub _get_output_dir { my ($self, $class_name) = @_; return $self->output_path if defined $self->output_path; return File::Basename::dirname($class_name->__meta__->module_path); } sub _navigation_info { my ($self, $cmd_class) = @_; my @navigation_info; if ($cmd_class eq $self->class_name) { push(@navigation_info, [$self->executable_name, undef]); } else { push(@navigation_info, [$cmd_class->command_name_brief, undef]); my $parent_class = $cmd_class->parent_command_class; while ($parent_class) { if ($parent_class eq $self->class_name) { my $uri = $self->_make_filename($self->executable_name); my $name = $self->executable_name; unshift(@navigation_info, [$name, $uri]); last; } else { my $uri = $self->_make_filename($parent_class->command_name); my $name = $parent_class->command_name_brief; unshift(@navigation_info, [$name, $uri]); } $parent_class = $parent_class->parent_command_class; } } if ($self->_index_filename) { unshift(@navigation_info, ["(Top)", $self->_index_filename]); } return @navigation_info; } sub _get_command_tree { my ($self, $command) = @_; my $src = "use $command"; eval $src; if ($@) { $self->error_message("Failed to load class $command: $@") unless $self->suppress_errors; return; } return if $command->_is_hidden_in_docs; my $module_name = $command; $module_name =~ s|::|/|g; $module_name .= '.pm'; my $input_path = $self->input_path ? $self->input_path : ''; my $module_path = $INC{$module_name}; $self->status_message("Loaded $command from $module_name at $module_path"); my $external = $module_path !~ /^$input_path\// ? 1 : 0; my $tree = { command => $command, sub_commands => [], module_path => $module_path, external => $external, parent_class => $command->parent_command_class || undef, description => $command->help_brief, }; if ($command eq $self->class_name) { $tree->{command_name} = $tree->{command_name_brief} = $self->executable_name; } else { $tree->{command_name} = $command->command_name; $tree->{command_name_brief} = $command->command_name_brief; } $tree->{uri} = $self->_make_filename($tree->{command_name}); if ($command->can("sub_command_classes")) { for my $cmd ($command->sub_command_classes) { my $subtree = $self->_get_command_tree($cmd); push(@{$tree->{sub_commands}}, $subtree) if $subtree; } } return $tree; } 1; RewriteClassHeader.pm000444023532023421 311512121654175 23470 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Update package UR::Namespace::Command::Update::RewriteClassHeader; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ force => { is => 'Boolean', is_optional => 1 }, ] ); sub params_as_getopt_specification { my $self = shift; my @spec = $self->SUPER::params_as_getopt_specification(@_); return (@spec, "force!"); } sub help_brief { "Update::RewriteClassHeaders class descriptions headers to normalize manual changes." } sub help_detail { qq| UR classes have a header at the top which defines the class in terms of its metadata. This command replaces that text in the source module with a fresh copy. It is most useful to fix formatting problems, since the data from which the new version is made is the data supplied by the old version of the file. It's somewhat of a "perltidy" for the module header. | } sub for_each_class_object { #$DB::single = 1; my $self = shift; my $class = shift; my $old = $class->module_header_source; my $new = $class->resolve_module_header_source; if ($self->force or ($old ne $new)) { print "Updating:\t", $class->module_base_name, "\n"; $class->rewrite_module_header and return 1; print STDERR "Error rewriting header!" and return 0; } else { #print $class->class_name . " has no source changes. " # . "Ignoring " . $class->module_base_name . ".\n"; return 1; } } 1; #$Header$ Test000755023532023421 012121654175 16752 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandRun.pm000444023532023421 7714112121654172 20240 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::Run; # # single dash command line params go to perl # double dash command line params go to the script # use warnings; use strict; use File::Temp; # qw/tempdir/; use Path::Class; # qw(file dir); use DBI; use Cwd; use UR; our $VERSION = "0.41"; # UR $VERSION; use File::Find; use TAP::Harness; use TAP::Formatter::Console; use TAP::Parser::Aggregator; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1, is_input => 1 }, recurse => { is => 'Boolean', doc => 'Run all .t files in the current directory, and in recursive subdirectories.' }, list => { is => 'Boolean', doc => 'List the tests, but do not actually run them.' }, noisy => { is => 'Boolean', doc => "doesn't redirect stdout",is_optional => 1 }, perl_opts => { is => 'String', doc => 'Override options to the Perl interpreter when running the tests (-d:Profile, etc.)', is_optional => 1, default_value => '' }, lsf => { is => 'Boolean', doc => 'If true, tests will be submitted as jobs via bsub' }, color => { is => 'Boolean', doc => 'Use TAP::Harness::Color to generate color output', default_value => 0 }, junit => { is => 'Boolean', doc => 'Run all tests with junit style XML output. (requires TAP::Formatter::JUnit)' }, ], has_optional => [ 'time' => { is => 'String', doc => 'Write timelog sum to specified file', }, long => { is => 'Boolean', doc => 'Run tests including those flagged as long', }, cover => { is => 'List', doc => 'Cover only this(these) modules', }, cover_svn_changes => { is => 'Boolean', doc => 'Cover modules modified in svn status', }, cover_svk_changes => { is => 'Boolean', doc => 'Cover modules modified in svk status', }, cover_cvs_changes => { is => 'Boolean', doc => 'Cover modules modified in cvs status', }, cover_git_changes => { is => 'Boolean', doc => 'Cover modules modified in git status', }, coverage => { is => 'Boolean', doc => 'Invoke Devel::Cover', }, script_opts => { is => 'String', doc => 'Override options to the test case when running the tests (--dump-sql --no-commit)', default_value => '' }, callcount => { is => 'Boolean', doc => 'Count the number of calls to each subroutine/method', }, jobs => { is => 'Number', doc => 'How many tests to run in parallel', default_value => 1, }, lsf_params => { is => 'String', doc => 'Params passed to bsub while submitting jobs to lsf', default_value => '-q short -R select[type==LINUX64]' }, run_as_lsf_helper => { is => 'String', doc => 'Used internally by the test harness', }, inc => { is => 'String', doc => 'Additional paths for @INC, alias for -I', is_many => 1, }, ], ); sub help_brief { "Run the test suite against the source tree." } sub help_synopsis { return <<'EOS' cd MyNamespace ur test run --recurse # run all tests in the namespace or under the current directory ur test run # runs all tests in the t/ directory under pwd ur test run t/mytest1.t My/Class.t # run specific tests ur test run -v -t --cover-svk-changes # run tests to cover latest svk updates ur test run -I ../some/path/ # Adds ../some/path to perl's @INC through -I ur test run --junit # writes test output in junit's xml format (consumable by Hudson integration system) EOS } sub help_detail { return <define_boolexpr(@_); unless ($bx->specifies_value_for('namespace_name')) { my $namespace_name = $class->resolve_namespace_name_from_cwd(); $namespace_name ||= 'UR'; # Pretend we're running in the UR namespace $bx = $bx->add_filter(namespace_name => $namespace_name); } return $class->SUPER::create($bx); } # Override so we'll allow '-I' on the command line sub _shell_args_getopt_specification { my $self = shift; my($params_hash, @spec) = $self->SUPER::_shell_args_getopt_specification(); foreach (@spec) { if ($_ eq 'inc=s@') { $_ = 'inc|I=s@'; last; } } return($params_hash, @spec); } sub execute { my $self = shift; #$DB::single = 1; my $working_path; if ($self->namespace_name ne 'UR') { $self->status_message("Running tests within namespace ".$self->namespace_name); $working_path = $self->namespace_path; } else { $self->status_message("Running tests under the current directory"); $working_path = '.'; } if ($self->run_as_lsf_helper) { $self->_lsf_test_worker($self->run_as_lsf_helper); exit(0); } # nasty parsing of command line args # this may no longer be needed.. my @tests = $self->bare_args; if ($self->recurse) { if (@tests) { $self->error_message("Cannot currently combine the recurse option with a specific test list."); return; } @tests = $self->_find_t_files_under_directory($working_path); } elsif (not @tests) { my @dirs; File::Find::find(sub { if ($_ eq 't' and -d $_) { push @dirs, $File::Find::name; } }, $working_path); if (@dirs == 0) { $self->error_message("No 't' directories found. Write some tests."); return; } chomp @dirs; for my $dir (@dirs) { push @tests, $self->_find_t_files_under_directory($dir); } } else { # rely on the @tests list from the cmdline } # uniqify and sort them my %tests = map { $_ => 1 } @tests; @tests = sort keys %tests; if ($self->list) { $self->status_message("Tests:"); for my $test (@tests) { $self->status_message($test); } return 1; } if (not @tests) { $self->error_message("No tests found under $working_path"); return; } my $results = $self->_run_tests(@tests); return $results; } sub _find_t_files_under_directory { my($self,$path) = @_; my @tests; File::Find::find(sub { if (m/\.t$/ and not -d $_) { push @tests, $File::Find::name; } }, $path); chomp @tests; return @tests; } # Run by the test harness when test are scheduled out via LSF # $master_spec is a string like "host:port" sub _lsf_test_worker { my($self,$master_spec) = @_; require IO::Socket; open my $saved_stdout, ">&STDOUT" or die "Can't dup STDOUT: $!"; open my $saved_stderr, ">&STDERR" or die "Can't dup STDERR: $!"; while(1) { open STDOUT, ">&", $saved_stdout or die "Can't restore stdout \$saved_stdout: $!"; open STDERR, ">&", $saved_stderr or die "Can't restore stderr \$saved_stderr: $!"; my $socket = IO::Socket::INET->new( PeerAddr => $master_spec, Proto => 'tcp'); unless ($socket) { die "Can't connect to test master: $!"; } $socket->autoflush(1); my $line = <$socket>; chomp($line); if ($line eq '' or $line eq 'EXIT TESTS') { # print STDERR "Closing\n"; $socket->close(); exit(0); } # print "Running >>$line<<\n"; open STDOUT, ">&", $socket or die "Can't redirect stdout: $!"; open STDERR, ">&", $socket or die "Can't redirect stderr: $!"; system($line); $socket->close(); } } sub _run_tests { my $self = shift; my @tests = @_; # this ensures that we don't see warnings # and error statuses when doing the bulk test no warnings; local $ENV{UR_TEST_QUIET} = $ENV{UR_TEST_QUIET}; unless (defined $ENV{UR_TEST_QUIET}) { $ENV{UR_TEST_QUIET} = 1; } use warnings; local $ENV{UR_DBI_NO_COMMIT} = 1; if($self->long) { # Make sure long tests run $ENV{UR_RUN_LONG_TESTS}=1; } my @cover_specific_modules; if (my $cover = $self->cover) { push @cover_specific_modules, @$cover; } if ($self->cover_svn_changes) { push @cover_specific_modules, get_status_file_list('svn'); } elsif ($self->cover_svk_changes) { push @cover_specific_modules, get_status_file_list('svk'); } elsif ($self->cover_git_changes) { push @cover_specific_modules, get_status_file_list('git'); } elsif ($self->cover_cvs_changes) { push @cover_specific_modules, get_status_file_list('cvs'); } if (@cover_specific_modules) { my $dbh = DBI->connect("dbi:SQLite:/gsc/var/cache/testsuite/coverage_metrics.sqlitedb","",""); $dbh->{PrintError} = 0; $dbh->{RaiseError} = 1; my %tests_covering_specified_modules; for my $module_name (@cover_specific_modules) { my $module_test_names = $dbh->selectcol_arrayref( "select test_name from test_module_use where module_name = ?",undef,$module_name ); for my $test_name (@$module_test_names) { $tests_covering_specified_modules{$test_name} ||= []; push @{ $tests_covering_specified_modules{$test_name} }, $module_name; } } if (@tests) { # specific tests were listed: only run the intersection of that set and the covering set my @filtered_tests; for my $test_name (sort keys %tests_covering_specified_modules) { my $specified_modules_coverted = $tests_covering_specified_modules{$test_name}; $test_name =~ s/^(.*?)(\/t\/.*)$/$2/g; if (my @matches = grep { $test_name =~ $_ } @tests) { if (@matches > 1) { Carp::confess("test $test_name matches multiple items in the tests on the filesystem: @matches"); } elsif (@matches == 0) { Carp::confess("test $test_name matches nothing in the tests on the filesystem!"); } else { print STDERR "Running $matches[0] for modules @$specified_modules_coverted.\n"; push @filtered_tests, $matches[0]; } } } @tests = @filtered_tests; } else { # no tests explicitly specified on the command line: run exactly those which cover the listed modules @tests = sort keys %tests_covering_specified_modules; } print "Running the " . scalar(@tests) . " tests which load the specified modules.\n"; } else { } use Cwd; my $cwd = cwd(); for (@tests) { s/^$cwd\///; } my $perl_opts = $self->perl_opts; if ($self->coverage()) { $perl_opts .= ' -MDevel::Cover'; } if ($self->callcount()) { $perl_opts .= ' -d:callcount'; } if (UR::Util::used_libs()) { $ENV{'PERL5LIB'} = UR::Util::used_libs_perl5lib_prefix() . $ENV{'PERL5LIB'}; } my %harness_args; my $formatter; if ($self->junit) { eval "use TAP::Formatter::JUnit;"; if ($@) { Carp::croak("Couldn't use TAP::Formatter::JUnit for junit output: $@"); } %harness_args = ( formatter_class => 'TAP::Formatter::JUnit', merge => 1, timer => 1, ); } else { $formatter = TAP::Formatter::Console->new( { jobs => $self->jobs, show_count => 1, color => $self->color, } ); $formatter->quiet(); %harness_args = ( formatter => $formatter ); } $harness_args{'jobs'} = $self->jobs if ($self->jobs > 1); if ($self->script_opts) { my @opts = split(/\s+/, $self->script_opts); $harness_args{'test_args'} = \@opts; } $harness_args{'multiplexer_class'} = 'My::TAP::Parser::Multiplexer'; $harness_args{'scheduler_class'} = 'My::TAP::Parser::Scheduler'; if ($self->perl_opts || $self->inc) { $harness_args{'switches'} = [ split(' ', $self->perl_opts), map { '-I' . Path::Class::Dir->new($_)->absolute } $self->inc]; } my $timelog_sum = $self->time(); my $timelog_dir; if ($timelog_sum) { $harness_args{'parser_class'} = 'My::TAP::Parser::Timer'; $timelog_sum = Path::Class::file($timelog_sum); $timelog_dir = Path::Class::dir(File::Temp::tempdir('.timelog.XXXXXX', DIR => '.', CLEANUP => 1)); My::TAP::Parser::Timer->set_timer_info($timelog_dir,\@tests); } my $harness = TAP::Harness->new( \%harness_args); if ($self->lsf) { # There doesn't seem to be a clean way (either by configuring the harness, # subclassing the harness or parser, or hooking to a callback) to pass # down the user's requested lsf params from here. So, looks like we # need to hack it through here. This means that multiple 'ur test' commands # running concurrently and using lsf will always use the last object's lsf_params. # though I doubt anyone would ever really need to do that... My::TAP::Parser::IteratorFactory::LSF->lsf_params($self->lsf_params); My::TAP::Parser::IteratorFactory::LSF->max_jobs($self->jobs); $harness->callback('parser_args', sub { my($args, $job_as_arrayref) = @_; $args->{'iterator_factory_class'} = 'My::TAP::Parser::IteratorFactory::LSF'; }); } my $aggregator = TAP::Parser::Aggregator->new(); $aggregator->start(); my $old_stderr; unless ($self->noisy) { open $old_stderr ,">&STDERR" or die "Failed to save STDERR"; open(STDERR,">/dev/null") or die "Failed to redirect STDERR"; } eval { no warnings; local %SIG = %SIG; delete $SIG{__DIE__}; $ENV{UR_DBI_NO_COMMIT} = 1; #$DB::single = 1; $SIG{'INT'} = sub { print "\n\nInterrupt.\nWaiting for running tests to finish...\n\n"; $My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT = 1; $SIG{'INT'} = 'DEFAULT'; #My::TAP::Parser::IteratorFactory::LSF->_kill_running_jobs(); #sleep(1); #$aggregator->stop(); #$formatter->summary($aggregator); #exit(0); }; #runtests(@tests); $harness->aggregate_tests( $aggregator, @tests ); }; unless ($self->noisy) { open(STDERR,">&", $old_stderr) or die "Failed to restore STDERR"; } $aggregator->stop(); if ($@) { $self->error_message($@); return; } else { if ($self->coverage()) { # FIXME - is this GSC-specific? system("chmod -R g+rwx cover_db"); system("/gsc/bin/cover | tee > coverage.txt"); } $formatter->summary($aggregator) if ($formatter); } if ($timelog_sum) { $timelog_sum->openw->print( sort map { $_->openr->getlines } $timelog_dir->children ); if (-z $timelog_sum) { unlink $timelog_sum; warn "Error producing time summary file!"; } $timelog_dir->rmtree; } return !$aggregator->has_problems; } sub get_status_file_list { my $tool = shift; my @status_data = eval { my $orig_cwd = cwd(); my @words = grep { length($_) } split("/",$orig_cwd); while (@words and ($words[-1] ne "GSC")) { pop @words; } unless (@words and $words[-1] eq "GSC") { die "Cannot find 'GSC' directory above the cwd. Cannot auto-run $tool status.\n"; } pop @words; my $vcs_dir = "/" . join("/", @words); unless (chdir($vcs_dir)) { die "Failed to change directories to $vcs_dir!"; } my @lines; if ($tool eq "svn" or $tool eq "svk") { @lines = IO::File->new("$tool status |")->getlines; } elsif ($tool eq "cvs") { @lines = IO::File->new("cvs -q up |")->getlines; } elsif ($tool eq "git") { @lines = IO::File->new("git diff --name-status |")->getlines; } else { die "Unknown tool $tool. Try svn, svk, cvs or git.\n"; } # All these tools have flags or other data with the filename as the last column @lines = map { (split(/\s+/))[-1] } @lines; unless (chdir($orig_cwd)) { die "Error changing directory back to the original cwd after checking file status with $tool."; } return @lines; }; if ($@) { die "Error checking version control status for $tool:\n$@"; } my @modules; for my $line (@status_data) { my ($status,$file) = ($line =~ /^(.).\s*(\S+)/); next if $status eq "?" or $status eq "!"; print "covering $file\n"; push @modules, $file; } unless (@modules) { die "Failed to find modified modules via $tool.\n"; } return @modules; } package My::TAP::Parser::Multiplexer; use base 'TAP::Parser::Multiplexer'; sub _iter { my $self = shift; my $original_iter = $self->SUPER::_iter(@_); return sub { for(1) { # This is a hack... # the closure _iter returns does a select() on the subprocess' output handle # which returns immediately after you hit control-C with no results, and the # existing code in there expects real results from select(). This way, we catch # the exception that happens when you do that, and give it a chance to try again my @retval = eval { &$original_iter }; if (index($@, q(Can't use an undefined value as an ARRAY reference))>= 0) { redo; } elsif ($@) { die $@; } return @retval; } }; } package My::TAP::Parser::IteratorFactory::LSF; use IO::Socket; use IO::Select; use base 'TAP::Parser::IteratorFactory'; # Besides being the factory for parser iterators, we're also the factory for # LSF jobs # In the TAP::* code, they mention that the iterator factory is never instantiated, # but may be in the future. When that happens, move this state info into the # object that gets created/initialized my $state = { 'listen' => undef, # The listening socket 'select' => undef, # select object for the listen socket idle_jobs => [], # holds a list of file handles of connected workers # running_jobs => [], # we're not tracking workers that are working for now... lsf_jobids => [], # jobIDs of the worker processes lsf_params => '', # params when running bsub max_jobs => 0, # Max number of jobs }; sub _kill_running_jobs { # The worker processes should notice when the master goes away, # but just in case, we'll kill them off foreach my $jobid ( @{$state->{'lsf_jobids'}} ) { print "bkilling LSF jobid $jobid\n"; `bkill $jobid`; } } END { my $exit_code = $?; &_kill_running_jobs(); $? = $exit_code; # restore the exit code, since the bkill commands set a different exit code } sub lsf_params { my $proto = shift; if (@_) { $state->{'lsf_params'} = shift; } return $state->{'lsf_params'}; } sub max_jobs { my $proto = shift; if (@_) { $state->{'max_jobs'} = shift; } return $state->{'max_jobs'}; } sub make_process_iterator { my $proto = shift; My::TAP::Parser::Iterator::Process::LSF->new(@_); } sub next_idle_worker { my $proto = shift; $proto->process_events(); while(! @{$state->{'idle_jobs'}} ) { my $did_create_new_worker = 0; if (@{$state->{'lsf_jobids'}} < $state->{'max_jobs'}) { $proto->create_new_worker(); $did_create_new_worker = 1; } sleep(1); my $count = $proto->process_events($did_create_new_worker ? 10 : 0); if (! $did_create_new_worker and ! $count) { unless ($proto->_verify_lsf_jobs_are_still_alive()) { print "\n*** The LSF worker jobs are having trouble starting up... Exiting\n"; kill 'INT', $$; sleep 2; kill 'INT', $$; } } } my $worker = shift @{$state->{'idle_jobs'}}; return $worker; } sub _verify_lsf_jobs_are_still_alive { my $alive = 0; foreach my $jobid ( @{$state->{'lsf_jobids'}} ) { my @output = `bjobs $jobid`; next unless $output[1]; # expired jobs only have 1 line of output: Job is not found my @stat = split(/\s+/, $output[1]); $alive++ if ($stat[2] eq 'RUN' or $stat[2] eq 'PEND'); } return $alive; } #sub worker_is_now_idle { # my($proto, $worker) = @_; # # for (my $i = 0; $i < @{$state->{'running_jobs'}}; $i++) { # if ($state->{'running_jobs'}->[$i] eq $worker) { # splice(@{$state->{'running_jobs'}}, $i, 1); # last; # } # } # # push @{$state->{'idle_workers'}}, $worker; #} sub create_new_worker { my $proto = shift; my $port = $state->{'listen'}->sockport; my $host = $state->{'listen'}->sockhost; if ($host eq '0.0.0.0') { $host = $ENV{'HOST'}; } $host .= ":$port"; my $lsf_params = $state->{'lsf_params'} || ''; my $line = `bsub $lsf_params ur test run --run-as-lsf-helper $host`; my ($jobid) = $line =~ m/Job \<(\d+)\>/; unless ($jobid) { Carp::croak("Couldn't parse jobid out of the line: $line"); } push @{$state->{'lsf_jobids'}}, $jobid; } sub process_events { my $proto = shift; my $timeout = shift || 0; my $listen = $state->{'listen'}; unless ($listen) { $listen = $state->{'listen'} = IO::Socket::INET->new(Listen => 5, Proto => 'tcp'); unless ($listen) { Carp::croak("Unable to create listen socket: $!"); } } my $select = $state->{'select'}; unless ($select) { $select = $state->{'select'} = IO::Select->new($listen); } my $processed_events = 0; while(1) { my @ready = $select->can_read($timeout); last unless (@ready); foreach my $handle ( @ready ) { $processed_events++; if ($handle eq $listen) { my $socket = $listen->accept(); unless ($socket) { Carp::croak("accept: $!"); } $socket->autoflush(1); push @{$state->{'idle_jobs'}}, $socket; } else { # shoulnd't get here... } $timeout = 0; # just do a poll() next time around } } return $processed_events; } package My::TAP::Parser::Timer; use base 'TAP::Parser'; our $timelog_dir; our $test_list; sub set_timer_info { my($class,$time_dir,$testlist) = @_; $timelog_dir = $time_dir; $test_list = $testlist; } sub make_iterator { my $self = shift; my $args = $_[0]; if (ref($args) eq 'HASH') { # It's about to make a process iterator. Prepend the stuff to # run the timer, too unless (-d $timelog_dir) { File::Path::mkpath("$timelog_dir"); } my $timelog_file = $self->_timelog_file_for_command_list($args->{'command'}); my $format = q('%C %e %U %S %I %K %P'); # yes, that's single quotes inside q() unshift @{$args->{'command'}}, '/usr/bin/time', '-o', $timelog_file, '-a', '-f', $format; } $self->SUPER::make_iterator(@_); } sub _timelog_file_for_command_list { my($self,$command_list) = @_; foreach my $test_file ( @$test_list ) { foreach my $cmd_part ( reverse @$command_list ) { if ($test_file eq $cmd_part) { my $log_file = Path::Class::file($cmd_part)->basename; $log_file =~ s/\.t$//; $log_file .= sprintf('.%d.%d.time', time(), $$); # Try to make the name unique $log_file = $timelog_dir->file($log_file); $log_file->openw->close(); return $log_file; } } } Carp::croak("Can't determine time log file for command line: ",join(' ',@$command_list)); } package My::TAP::Parser::Scheduler; use base 'TAP::Parser::Scheduler'; sub get_job { my $self = shift; if ($My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT) { our $already_printed; unless ($already_printed) { print "\n\n ",$self->{'count'}," Tests not yet run before interrupt\n"; print "------------------------------------------\n"; foreach my $job ( $self->get_all ) { print $job->{'description'},"\n"; } print "------------------------------------------\n"; $already_printed = 1; } return; } $self->SUPER::get_job(@_); } package My::TAP::Parser::Iterator::Process::LSF; our $SHOULD_EXIT = 0; use base 'TAP::Parser::Iterator::Process'; sub _initialize { my($self, $args) = @_; my @command = @{ delete $args->{command} || [] } or die "Must supply a command to execute"; # From TAP::Parser::Iterator::Process my $chunk_size = delete $args->{_chunk_size} || 65536; if ( my $setup = delete $args->{setup} ) { $setup->(@command); } my $handle = My::TAP::Parser::IteratorFactory::LSF->next_idle_worker(); # Tell the worker to run the command unless($handle->print(join(' ', @command) . "\n")) { print "Couldn't send command to worker on host ".$handle->peeraddr." port ".$handle->peerport.": $!\n"; print "Handle is " . ( $handle->connected ? '' : '_not_' ) . " connected\n"; } $self->{'out'} = $handle; $self->{'err'} = ''; $self->{'sel'} = undef; #IO::Select->new($handle); $self->{'pid'} = undef; $self->{'chunk_size'} = $chunk_size; if ( my $teardown = delete $args->{teardown} ) { $self->{teardown} = sub { $teardown->(@command); }; } return $self; } sub next_raw { my $self = shift; My::TAP::Parser::IteratorFactory::LSF->process_events(); if ($SHOULD_EXIT) { #$DB::single = 1; if ($self->{'sel'}) { foreach my $h ( $self->{'sel'}->handles ) { $h->close; $self->{'sel'}->remove($h); } return "1..0 # Skipped: Interrupted by user"; } else { return; } } $self->SUPER::next_raw(@_); } #sub _finish { # my $self = shift; # # $self->SUPER::_finish(@_); # # My::TAP::Parser::IteratorFactory::LSF->worker_is_now_idle($handle); #} 1; =pod =head1 NAME ur test run - run one or more test scripts =head1 SYNOPSIS # run everything in a given namespace cd my_sandbox/TheNamespace ur test run --recurse # run only selected tests cd my_sandbox/TheNamespace ur test run My/Module.t Another/Module.t t/foo.t t/bar.t # run only tests which load the TheNamespace::DNA module cd my_sandbox/TheNamespace ur test run --cover TheNamespace/DNA.pm # run only tests which cover the changes you have in Subversion cd my_sandbox/TheNamespace ur test run --cover-svn-changes # run 5 tests in parallel as jobs scheduled via LSF cd my_sandbox/TheNamespace ur test run --lsf --jobs 5 =head1 DESCRIPTION Runs a test harness around automated test cases, like "make test" in a make-oriented software distrbution, and similar to "prove" run in bulk. When run w/o parameters, it looks for "t" directory in the current working directory, and runs ALL tests under that directory. =head1 OPTIONS =over 4 =item --recurse Run all tests in the current directory, and in sub-directories. Without --recurse, it will first recursively search for directories named 't' under the current directory, and then recursively seatch for *.t files under those directories. =item --long Include "long" tests, which are otherwise skipped in test harness execution =item -v Be verbose, meaning that individual cases will appear instead of just a full-script summary =item --cover My/Module.pm Looks in a special sqlite database which is updated by the cron which runs tests, to find all tests which load My/Module.pm at some point before they exit. Only these tests will be run. * you will still need the --long flag to run long tests. * if you specify tests on the command-line, only tests in both lists will run * this can be specified multiple times =item --cover-TOOL-changes TOOL can be svn, svk, or cvs. The script will run either "svn status", "svk status", or "cvs -q up" on a parent directory with "GSC" in it, and get all of the changes in your perl_modules trunk. It will behave as though those modules were listed as individual --cover options. =item --lsf Tests should not be run locally, instead they are submitted as jobs to the LSF cluster with bsub. =item --lsf-params Parameters given to bsub when sceduling jobs. The default is "-q short -R select[type==LINUX64]" =item --jobs This many tests should be run in parallel. If --lsf is also specified, then these parallel tests will be submitted as LSF jobs. =back =head1 PENDING FEATURES =over =item automatic remote execution for tests requiring a distinct hardware platform =item logging profiling and coverage metrics with each test =over 4 =back =cut Window.pm000444023532023421 1361512121654172 20737 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Test use strict; use warnings; use above 'UR'; package UR::Namespace::Command::Test::Window; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'Command', has => { src => { is_optional => 1, is_many => 1, shell_args_position => 1 } }, doc => 'repl tk window' ); sub execute { my $self = shift; require Tk; my $src = [$self->src]; if (@$src > 0) { for my $code (@$src) { no strict; no warnings; eval $code; print $@; } } else { UR::Namespace::Command::Test::Window::Tk->activate_with_gtk_support; } } package UR::Namespace::Command::Test::Window::Tk;; our $tmp; our $workspace; sub new { if ($^O eq 'MSWin32' || $^O eq 'cygwin') { #$tmp = $ENV{TEMP}; $tmp ||= 'C:/temp'; } else { $tmp = $ENV{TMPDIR}; $tmp ||= '/tmp'; } # Make a window my $debug_window = new MainWindow(-title=>"Debug"); # The top pane is for inputing Perl my $in = $debug_window->Scrolled("Text", -scrollbars=>'os',-exportselection => 1)->pack(-expand => 0, -fill => 'x'); # The middle is a frame with an eval button my $frame = $debug_window->Frame()->pack(-expand=>0,-fill=>'both',-anchor=>'nw'); my $go = $frame->Button(-text => 'eval()', -command => sub { &exec_debug($debug_window) } )->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); # The bottom is a pane for output my $out = $debug_window->Scrolled("Text", -scrollbars=>'osoe',-wrap => 'none')->pack(-fill => 'both', -expand => 1); # See if there is a workspace file for an app with this name my $user = $ENV{USER}; $user ||= 'anonymous'; $0 =~ /([^\/\s]+)$/; my $core_name = $1; $workspace ||= "$user\@$core_name"; print STDOUT "Workspace is $workspace\n"; if (open(LAST_WORKSPACE,"${tmp}/$workspace")) { while () { $in->insert("end",$_); } close LAST_WORKSPACE; } $debug_window->{in} = $in; $debug_window->{out} = $out; return $debug_window; } sub new_gtk { require Gtk; my $debug_window = DebugWindow::new(); my $frame = $debug_window->Frame()->pack(-expand=>0,-fill=>'both',-anchor=>'nw'); my $continuous_refresh = 0; $frame->Button(-text => 'One Gtk', -command => sub { Gtk->main_iteration })->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); $frame->Button(-text => 'All Gtk', -command => sub { while (Gtk->events_pending) { Gtk->main_iteration; } })->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); my $handleGtk; $handleGtk = sub { return unless (Exists($debug_window) and $continuous_refresh); Gtk->main_iteration; my $delay = (Gtk->events_pending ? 5 : 500); Tk->after($delay, $handleGtk); }; $frame->Button ( -text => 'Gtk Cont', -command => sub { $continuous_refresh = (not $continuous_refresh); &$handleGtk; } )->pack(-expand => 0, -fill => 'none', -anchor=>'nw', -side=>'left'); } sub activate { my $window=&new; $window->waitWindow(); Tk->MainLoop; } sub activate_with_gtk_support { &new_gtk; Tk->MainLoop; } sub hook_button { } sub hook_gtk_button { my $gtk_button = shift; $gtk_button->signal_connect('button_press_event', sub { my ($self,$event) = @_; # Test the Gtk widget event to see which button was clicked. if ($event->{button} == 3) { # Instantiate the debug window with the special Gtk buttons on it. my $debug_window = DebugWindow::new_gtk(); Tk->MainLoop(); } return(1); }); } sub show_new { # Legacy function &new(@_); } sub exec_debug { my $self = $_[0]; my $in = $self->{in}; my $out = $self->{out}; # Clear the results window. $out->delete("1.0","end"); # Get all of the text in the workspace window. my $perl = $in->get("1.0","end"); # If there is a valid selection override the above with just the selected text. eval { $perl = $in->get("sel.first","sel.last"); }; # Open a temporary output file to catch the STDOUT my $filename = "${tmp}/${workspace}_output"; open (DEBUG_FH,">$filename") or die "Failed to open temp file '$filename': $!\n"; # Redirect STDOUT temporarily *ORIG_STDOUT = *STDOUT; *STDOUT = *DEBUG_FH; # Run the perl. eval ("package main;\n" . $perl); # Print any errors print $@; # Restore STDOUT *STDOUT = *ORIG_STDOUT; close DEBUG_FH; # Get the script output my $fh = IO::File->new("${tmp}/${workspace}_output"); my $text; if ($fh) { my @text = $fh->getlines; $fh->close; $text = join("",@text); } # Print to the console as well as the result widget. print $text; # For some reason embedded \n causese every other row to disappear. # Split on line boundaries and feed the output to the widget in pieces. foreach my $row (split /$/, $text) { $out->insert('end',$row); } # Save the whole workspace like we do when the app closes save_workspace($self); } sub save_workspace { my $self = $_[0]; my $in = $self->{in}; # Save the workspace if (open (SCRIPT_FH, ">${tmp}/$workspace")) { print SCRIPT_FH $in->get("1.0","end"); close SCRIPT_FH; print "Saved to ${tmp}/$workspace\n"; } else { print STDOUT "Failed to save the current workspace (${tmp}/$workspace)!"; } } 1; Eval.pm000444023532023421 216112121654173 20332 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Eval; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Evaluate a string of Perl source"; } sub help_synopsis { return <<'EOS'; ur test eval 'print "hello\n"' ur test eval 'print "hello\n"' 'print "goodbye\n"' ur test eval 'print "Testing in the " . \$self->namespace_name . " namespace.\n"' EOS } sub help_detail { return <bare_args) { eval "use Data::Dumper; use YAML; no strict; no warnings; \n" . $src; if ($@) { print STDERR "EXCEPTION:\n$@"; } } return 1; } 1; Callcount.pm000444023532023421 1532612121654174 21417 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::Callcount; use warnings; use strict; use IO::File; use File::Find; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has => [ 'sort' => { is => 'String', valid_values => ['count', 'sub'], default_value => 'count', doc => 'The output file should be sorted by "count" (sub call counts) or "sub" (sub names)' }, ], has_optional => [ input => { is => 'ARRAY', doc => 'list of input file pathnames' }, output => { is => 'String', doc => 'pathname of the output file' }, bare_args => { is_many => 1, shell_args_position => 1 } ], ); sub help_brief { "Collect the data from a prior 'ur test run --callcount' run into a single output file" } sub help_synopsis { return <input; if ($inputs and ref($inputs) eq 'ARRAY') { @input = @$inputs; } elsif ($inputs and $inputs =~ m/,/) { @input = split(',',$inputs); } elsif (!$inputs) { @input = $self->bare_args; @input = ('.') unless @input; # when no inputs at all are given, start with '.' } else { $self->error_message("Couldn't determine input files and directories"); return; } # Now, flatten out everything in @input by searching in directories # for *.callcount files my(@directories, %input_files); foreach (@input) { if (-d $_) { push @directories, $_; } else { $input_files{$_} = 1; } } if (@directories) { my $wanted = sub { if ($File::Find::name =~ m/.callcount$/) { $input_files{$File::Find::name} = 1; } }; File::Find::find($wanted, @directories); } my $out_fh; if ($self->output and $self->output eq '-') { $out_fh = \*STDOUT; } elsif ($self->output) { my $output = $self->output; $out_fh = IO::File->new($output, 'w'); unless ($out_fh) { $self->error_message("Can't open $output for writing: $!"); return undef; } } my %data; foreach my $input_file ( keys %input_files ) { my $in_fh = IO::File->new($input_file); unless ($in_fh) { $self->error_message("Can't open $input_file for reading: $!"); next; } while(<$in_fh>) { chomp; my($count, $subname, $subloc, $callers) = split(/\t/, $_, 4); $callers ||= ''; my %callers; foreach my $caller ( split(/\t/, $callers ) ) { $callers{$caller} = 1; } if (exists $data{$subname}) { $data{$subname}->[0] += $count; foreach my $caller ( keys %callers ) { $data{$subname}->[3]->{$caller} = 1; } } else { $data{$subname} = [ $count, $subname, $subloc, \%callers]; } } $in_fh->close(); } my @order; if ($self->sort eq 'count') { @order = sort { $a->[0] <=> $b->[0] } values %data; } elsif ($self->sort eq 'sub' or $self->sort eq 'subs') { @order = sort { $a->[1] cmp $b->[1] } values %data; } if ($out_fh) { foreach ( @order ) { my $callers = join("\t", keys %{$_->[3]}); # convert the callers back into a \t sep string $out_fh->print(join("\t",@{$_}[0..2], $callers), "\n"); } $out_fh->close(); } return \@order; } 1; =pod =head1 NAME B - collect callcount data from running tests into one file =head1 SYNOPSIS # run tests in a given namespace cd my_sandbox/TheApp ur test run --recurse --callcount ur test callcount --output all_tests.callcount =head1 DESCRIPTION Callcount data can be used to find unused subroutines in your code. When the test suite is run with the C option, then for each *.t file run by the test suite, a corresponding *.callcount file is created containing information about how often all the defined subroutines were called. The callcount file is a plain text file with three columns: =over 4 =item 1. The number of times this subroutine was called =item 2. The name of the subroutine =item 3. Where in the code this subroutine is defined =back After a test suite run with sufficient coverage, subroutines with 0 calls are candidates for removal, and subs with high call counts are candidates for optimization. =head1 OPTIONS =over 4 =item --input Name the *.callcount input file(s). When run from the command line, it accepts a list of files separated by ','s. Input files can also be given as plain, unnamed command line arguments (C). When run as a command module within another program, the C) property can be an arrayref of pathanmes. After inputs are determined, any directories given are expanded by searching them recursively for files ending in .callcount with L. If no inputs in any form are given, then it defaults to '.', the current directory, which means all *.callcount files under the current directory are used. =item --output The pathname to write the collected data to. The user may use '-' to print the results to STDOUT. =item --sort How the collected results should be sorted before being reported. The default is 'count', which sorts incrementally by call count (the first column). 'sub' performs a string sort by subroutine name (column 2). =back =head1 execute() The C method returns an arrayref of data sorted in the appropriate way. Each element is itself an arrayref of three items: count, sub name, and sub location. =cut TrackObjectRelease.pm000444023532023421 641612121654174 23147 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Testpackage UR::Namespace::Command::Test::TrackObjectRelease; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; class UR::Namespace::Command::Test::TrackObjectRelease { is => 'UR::Namespace::Command::Base', has => [ file => { is => 'Text', doc => 'pathname of the input file' }, ], }; sub help_brief { 'Parse the data produced by UR_DEBUG_OBJECT_RELEASE and report possible memory leaks' }; sub help_synopsis { "ur test track-object-release --file /path/to/text.file > /path/to/results" } sub help_detail { "When a UR-based program is run with the UR_DEBUG_OBJECT_RELEASE environment variable set to 1, it will emit messages to STDERR describing the various stages of releasing an object. This command parses those messages and provides a report on objects which did not completely deallocate themselves, usually because of a reference being held." } sub execute { my $self = shift; #$DB::single = 1; my $file = $self->file; my $fh = IO::File->new($file,'r'); unless ($fh) { $self->error_message("Can't open input file: $!"); return; } # for a given state, it's legal predecessor my %prev_states = ( 'PRUNE object' => '', 'DESTROY object' => 'PRUNE object', 'UNLOAD object' => 'DESTROY object', 'DELETE object' => 'UNLOAD object', 'BURY object' => 'DELETE object', 'DESTROY deletedref' => 'BURY object', ); my %next_states = reverse %prev_states; # After this we stop stracking it my %terminal_states = ( 'DESTROY deletedref' => 1 ); my %objects; while(<$fh>) { chomp; my ($action,$refaddr); if (m/MEM ((PRUNE|DESTROY|UNLOAD|DELETE|BURY) (object|deletedref)) (\S+)/) { $action = $1; my $refstr = $4; ($refaddr) = ($refstr =~ m/=HASH\((.*)\)/); } else { next; } my($class,$id) = m/class (\S+) id (.*)/; # These don't appear in the deletedref line, and are optional my $expected_prev_state = $prev_states{$action}; if (defined $expected_prev_state && $expected_prev_state) { # This state must have a predecessor if ($objects{$expected_prev_state}->{$refaddr}) { if ($terminal_states{$action}) { delete $objects{$expected_prev_state}->{$refaddr}; } else { $objects{$action}->{$refaddr} = delete $objects{$expected_prev_state}->{$refaddr}; } } else { print STDERR "$action for $refaddr without matching $expected_prev_state at line $.\n"; } } elsif (defined $expected_prev_state) { # The initial state $objects{$action}->{$refaddr} = $_; } else { print STDERR "Unknown action $action at line $.\n"; } } foreach my $action (keys %objects) { if (keys %{$objects{$action}} ) { print "\n$action but not $next_states{$action}\n"; foreach (keys %{$objects{$action}}) { print "$_ : ",$objects{$action}->{$_},"\n"; } } } return 1; } 1; Use.pm000444023532023421 702412121654175 20204 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Use; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use Cwd; use YAML; class UR::Namespace::Command::Test::Use { is => "UR::Namespace::Command::RunsOnModulesInTree", has_optional => [ verbose => { is => 'Boolean', doc => 'List each explicitly.' }, summarize_externals => { is => 'Boolean', doc => 'List all modules used which are outside the namespace.' }, exec => { is => 'Text', doc => 'Execute the specified Perl _after_ using all of the modules.' }, ] }; sub help_brief { "Tests each module for compile errors by 'use'-ing it. Also reports on any libs added to \@INC by any modules (bad!)." } sub help_synopsis { return <_help_detail_footer; return $text; } sub before { my $self = shift; $self->{success} = 0; $self->{failure} = 0; $self->{used_libs} = {}; $self->{used_mods} = {}; $self->{failed_libs} = []; $self->{default_print_fh} = fileno(select); $self->SUPER::before(@_); } sub for_each_module_file { my $self = shift; my $module_file = shift; my $namespace_name = $self->namespace_name; my %libs_before = map { $_ => 1 } @INC; my %mods_before = %INC if $self->summarize_externals; local $SIG{__DIE__}; local $ENV{UR_DBI_MONITOR_SQL} = 1; local $ENV{APP_DBI_MONITOR_SQL} = 1; eval "require '$module_file'"; my %new_libs = map { $_ => 1 } grep { not $libs_before{$_} } @INC; my %new_mods = map { $_ => $module_file } grep { not $_ =~ /^$namespace_name\// } grep { not $mods_before{$_} } keys %INC; if (%new_libs) { $self->{used_libs}{$module_file} = \%new_libs; } if (%new_mods) { for my $mod (keys %new_mods) { $self->{used_mods}{$mod} = $module_file; } } if ($@) { print "$module_file FAILED:\n$@\n"; $self->{failure}++; push @{$self->{failed_libs}}, $module_file; } elsif (fileno(select) != $self->{default_print_fh}) { # un-steal the default file handle back select(STDOUT); print "$module_file FAILED DUE TO IMPROPER FILEHANDLE USE\n"; $self->{failure}++; push @{$self->{failed_libs}}, $module_file; } else { print "$module_file OK\n" if $self->verbose; $self->{success}++; } return 1; } sub after { my $self = shift; $self->status_message("SUCCESS: $self->{success}"); $self->status_message("FAILURE: $self->{failure}"); if ($self->{failure} > 0) { $self->status_message("FAILED LIBS: " . YAML::Dump($self->{failed_libs})); } if (%{ $self->{used_libs} }) { $self->status_message( "ROGUE LIBS: " . YAML::Dump($self->{used_libs}) ) } if ($self->summarize_externals) { $self->status_message( "MODULES USED: " . YAML::Dump($self->{used_mods}) ); } if (my $src = $self->exec) { eval $src; $self->error_message($@) if $@; } return if $self->{failure}; return 1; } 1; Compile.pm000444023532023421 233312121654175 21036 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Test package UR::Namespace::Command::Test::Compile; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_brief { "Attempts to compile each module in the namespace in its own process." } sub help_synopsis { return <lib_path; my @response = `cd $lib_path; perl -I $lib_path -c $module_file 2>&1`; if (grep { $_ eq "$module_file syntax OK\n" } @response) { print "$module_file syntax OK\n" } else { chomp @response; print "$module_file syntax FAILED\n" . join("\n\t",@response), "\n"; } return 1; } 1; Callcount000755023532023421 012121654175 20676 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/TestList.pm000444023532023421 347512121654175 22315 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Test/Callcountpackage UR::Namespace::Command::Test::Callcount::List; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; # Transient class that represents the file as a datasource our $TheFile = '/dev/null'; # This will be filled in during create() below UR::DataSource::FileMux->create( id => 'Test::Callcount::List::DataSource', column_order => ['count','subname','subloc','callers'], delimiter => "\t", file_resolver => sub { return $TheFile }, required_for_get => [], constant_values => [], ); #class Test::Callcount::List::DataSource { # is => 'UR::DataSource::File', # column_order => ['count','subname','subloc','callers'], # delimiter => "\t", #}; # Transient class that represents the data in the callcount files class Test::Callcount::List::Items { id_by => 'subname', has => [ count => { is => 'Integer' }, subname => { is => 'String' }, subloc => { is => 'String' }, callers => { is => 'String' }, ], data_source => 'Test::Callcount::List::DataSource', }; # Class for this command class UR::Namespace::Command::Test::Callcount::List { is => 'UR::Object::Command::List', has => [ file => { is => 'String', doc => 'Specify the .callcount file', default_value => '/dev/null' }, subject_class_name => { is_constant => 1, value => 'Test::Callcount::List::Items' }, show => { default_value => 'count,subname,subloc,callers' }, # filter => { default_value => '' }, ], doc => 'Filter and list Callcount items', }; sub _resolve_boolexpr { my $self = shift; my $filename = $self->file; unless (-r $filename ) { $self->error_message("File $filename does not exist or is not readable"); return; } $TheFile = $filename; $self->SUPER::_resolve_boolexpr(@_); } 1; Old000755023532023421 012121654175 16551 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandDiffRewrite.pm000444023532023421 71212121654172 21433 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::DiffRewrite; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", ); sub help_description { "Show the differences between current class headers and the results of a rewrite." } *for_each_class_object = \&UR::Namespace::Command::Diff::for_each_class_object_delegate_used_by_sub_commands; 1; ExportDbicClasses.pm000444023532023421 344712121654172 22632 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Oldpackage UR::Namespace::Command::Old::ExportDbicClasses; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Create or update a DBIx::Class class from an already existing UR class"; } sub help_detail { return <bare_args) { $self->error_message("No class names were specified on the command line"); $self->status_message($self->help_usage_complete_text,"\n"); return; } my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } eval "use $namespace"; if ($@) { $self->error_message("Failed to load namespace $namespace"); return; } foreach my $class_name ( $self->bare_args ) { my $class = UR::Object::Type->get(class_name => $class_name); unless ($class) { $self->error_message("Couldn't load class metadata for $class_name"); next; } $class->dbic_rewrite_module_header(); } return 1; } sub for_each_class_object { my($self,$class) = @_; return 1 unless ($class->table_name); # Skip classes without tables $class->dbic_rewrite_module_header(); return 1; } 1; Info.pm000444023532023421 642412121654173 20143 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Oldpackage UR::Namespace::Command::Old::Info; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', has => [ subject => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] ); sub help_brief { "Outputs description(s) of UR entities such as classes and tables to stdout"; } sub is_sub_command_delegator { 0;} sub execute { my($self, $params) = @_; my $namespace = $self->namespace_name; # FIXME why dosen't require work here? eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } # Loop through each command line parameter and see what kind of thing it is # create a view and display it my @class_aspects = qw( ); my @table_aspects = qw( ); my %already_printed; my %views; foreach my $item ( $self->subject ) { my @meta_objs = (); if ($item eq $namespace or $item =~ m/::/) { # Looks like a class name? my $class_meta = eval { UR::Object::Type->get(class_name => $item)}; push(@meta_objs, $class_meta) if $class_meta; } else { push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => $item, namespace => $namespace) ); push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => uc($item), namespace => $namespace) ); push @meta_objs, ( UR::DataSource::RDBMS::Table->get(table_name => lc($item), namespace => $namespace) ); push @meta_objs, map { ( $_ and UR::DataSource::RDBMS::Table->get(table_name => $_->table_name, namespace => $namespace) ) } ( UR::DataSource::RDBMS::TableColumn->get(column_name => $item, namespace => $namespace), UR::DataSource::RDBMS::TableColumn->get(column_name => uc($item), namespace => $namespace), UR::DataSource::RDBMS::TableColumn->get(column_name => lc($item), namespace => $namespace) ); } ## A property search requires loading all the classes first, at least until class ## metadata is in the meta DB # Something is making this die, so I'll comment it out for now #$namespace->get_material_class_names; #my @properties = UR::Object::Property->get(property_name => $item); #next unless @properties; #push @meta_objs, UR::Object::Type->get(class_name => [ map { $_->class_name } # @properties ]); foreach my $obj ( @meta_objs ) { next unless $obj; next if ($already_printed{$obj}++); $views{$obj->class} ||= UR::Object::View->create( subject_class_name => $obj->class, perspective => 'default', toolkit => 'text', ); my $view = $views{$obj->class}; $view->subject($obj); $view->show(); print "\n"; } } } 1; Redescribe.pm000444023532023421 114012121654175 21307 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::Redescribe; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::RunsOnModulesInTree', ); sub help_brief { "Outputs class description(s) formatted to the latest standard." } sub for_each_class_object { my $self = shift; my $class = shift; my $src = $class->resolve_module_header_source; if ($src) { print $src, "\n"; return 1; } else { print STDERR "No source for $class!"; return; } } 1; DiffUpdate.pm000444023532023421 66512121654175 21246 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Old package UR::Namespace::Command::Old::DiffUpdate; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", ); sub help_description { "Show the differences between class schema and database schema." } *for_each_class_object = \&UR::Namespace::Command::Diff::for_each_class_object_delegate_used_by_sub_commands; 1; Define000755023532023421 012121654175 17225 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandDb.pm000444023532023421 1613712121654173 20273 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Db; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; # required to import symbols used below UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has_input => [ uri => { is => 'Text', shell_args_position => 1, doc => 'a DBI connect string like dbi:mysql:someserver or user/passwd@dbi:Oracle:someserver~defaultns' }, name => { is => 'Text', shell_args_position => 2, default_value => 'Db1', doc => "the name for this data source (used for class naming)", }, ], has_output_optional => [ _class_name=> { is => 'Text', calculate_from => ['name'], calculate => q| my $namespace = $self->namespace_name; my $dsid = $namespace . '::DataSource::' . $name; return $dsid |, doc => "The full class name to give this data source.", }, _ds => { is_transient => 1, }, ], doc => 'add a data source to the current namespace' ); sub sub_command_sort_position { 2 } sub help_synopsis { return <<'EOS' ur define db dbi:SQLite:/some/file.db Db1 ur define db me@dbi:mysql:myserver MainDb ur define db me@dbi:Oracle:someserver ProdDb ur define db me@dbi:Oracle:someserver~schemaname BigDb ur define db me@dbi:Pg:prod Db1 ur define db me@dbi:Pg:dev Testing::Db1 # alternate for "Testing" (arbitrary) context ur define db me@dbi:Pg:stage Staging::Db1 # alternate for "Staging" (arbitrary) context EOS } sub data_source_module_pathname { my $self = shift; my $class_name = shift; my $ns_path = $self->namespace_path; my @ds_parts = split(/::/, $class_name); shift @ds_parts; # Get rid of the namespace name my $filename = pop @ds_parts; $filename .= '.pm'; my $path = join('/', $ns_path, @ds_parts, $filename); return $path; } sub execute { my $self = shift; my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } my $uri = $self->uri; my ($protocol,$driver,$login,$server,$owner) = ($uri =~ /^([^\:\W]+):(.*?):(.*@|)(.*?)(~.*|)$/); unless ($protocol) { $self->error_message("error parsing URI $uri\n" . 'expected dbi:$driver:$user@$server with optional trailing ~$namespace'); return; } unless ($protocol eq 'dbi') { $self->error_message("currently only the 'dbi' protocol is supported with this command. Other data sources must be hand-written."); return; } $login =~ s/\@$// if defined $login; $owner =~ s/^~// if defined $owner; $self->status_message("protocol: $protocol"); $self->status_message("driver: $driver"); $self->status_message("server: $server"); my $password; if (defined $login) { if ($login =~ /\//) { ($login,$password) = split('/',$login); } $self->status_message("login: $login") if defined $login; $self->status_message("password: $password") if defined $password; } $self->status_message("owner: $owner") if defined $owner; # Force an autoload of the namespace module eval "use $namespace"; if ($@) { $self->error_message("Can't load namespace $namespace: $@"); return; } my $class_name = $self->namespace_name . '::DataSource::' . $self->name; $self->_class_name($class_name); my $c = eval { UR::DataSource->get($class_name) || $class_name->get() }; if ($c) { $self->error_message("A data source named $class_name already exists\n"); return; } my $src = "package $class_name;\nuse strict;\nuse warnings;\nuse $namespace;\n\n"; $src .= "class $class_name {\n"; my $parent_ds_class = 'UR::DataSource::' . $driver; #$self->_data_source_sub_class_name(); $driver =~ s/mysql/MySQL/g; my @parent_classes = ( $parent_ds_class ); push @parent_classes, 'UR::Singleton'; $src .= sprintf(" is => [ '%s' ],\n", join("', '", @parent_classes)); $src .= "};\n"; my $module_body = $self->_resolve_module_body($class_name,$namespace,$driver,$server,$login); $src .= "\n$module_body\n1;\n"; my $module_path = $self->data_source_module_pathname($class_name); my $fh = IO::File->new($module_path, O_WRONLY | O_CREAT | O_EXCL); unless ($fh) { $self->error_message("Can't open $module_path for writing: $!"); return; } $fh->print($src); $fh->close(); $self->status_message("A $class_name (" . join(',', @parent_classes) . ")\n"); #TODO: call a method on the datasource to init the new file my $method = '_post_module_written_' . lc($driver); $self->$method($module_path,$server); unless (UR::Object::Type->use_module_with_namespace_constraints($class_name)) { #if ($@) { $self->error_message("Error in module $class_name!?: $@"); return; } my $ds = $class_name->get(); unless ($ds) { $self->error_message("Failed to get data source for $class_name!"); return; } $self->_ds($ds); if ($self->_try_connect()) { return 1; } else { return; } } sub _resolve_module_body { my ($self,$class_name,$namespace,$driver,$server,$login,$owner) = @_; $owner ||= $login; my $src = <new($server, O_WRONLY | O_CREAT) unless (-f $server); $self->status_message("A $server (empty database schema)"); $pathname =~ s/\.pm$/.sqlite3/; unless ($pathname eq $server) { symlink ($server, $pathname) or die "no symline $pathname for $server! $!"; } return 1; } sub _post_module_written_pg { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_oracle { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_mysql { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_file { my ($self, $pathname, $server) = @_; return 1; } sub _post_module_written_filemux { my ($self, $pathname, $server) = @_; return 1; } sub _try_connect { my $self = shift; $self->status_message(" ...connecting..."); my $ds = $self->_ds; my $dbh = $ds->get_default_handle(); if ($dbh) { $self->status_message(" ....ok\n"); return 1; } else { $self->error_message(" ERROR: " . $ds->error_message); return; } } 1; Class.pm000444023532023421 427712121654173 20775 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Class; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Namespace::Command::Define::Class { is => 'UR::Namespace::Command::Base', has => [ names => { is_optional => 1, is_many => 1, shell_args_position => 1 }, extends => { doc => "The base class. Defaults to UR::Object.", default_value => 'UR::Object' }, ], doc => 'Add one or more classes to the current namespace' }; sub sub_command_sort_position { 3 } sub help_synopsis { return <<'EOS' $ cd Acme $ ur define class Animal Vegetable Mineral A Acme::Animal A Acme::Vegetable A Acme::Mineral $ ur define class Dog Cat Bird --extends Animal A Acme::Dog A Acme::Cat A Acme::Bird EOS } sub execute { my $self = shift; my @class_names = $self->names; unless (@class_names) { $self->error_message("No class name(s) provided!"); return; } my $namespace = $self->namespace_name; my $is = $self->extends || 'UR::Object'; my $parent_class_meta = UR::Object::Type->get($is); unless ($parent_class_meta) { unless ($self->extends =~ /^${namespace}::/) { $parent_class_meta = UR::Object::Type->get($namespace . '::' . $is); if ($parent_class_meta) { $is = $namespace . '::' . $is; } } unless ($parent_class_meta) { $self->error_message("Failed to find base class $is!"); return; } } for my $class_name (@class_names) { unless ($class_name =~ /^${namespace}::/) { $class_name = $namespace . '::' . $class_name; } my $new_class = UR::Object::Type->create( class_name => $class_name, is => $is, ); unless ($new_class) { $self->error_message("Failed to create class $class_name!: " . UR::Object::Type->error_message ); return; } print "A $class_name\n"; $new_class->rewrite_module_header or die "Failed to write class $class_name!: " . $new_class->error_message; } return 1; } 1; Namespace.pm000444023532023421 521012121654174 21611 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Definepackage UR::Namespace::Command::Define::Namespace; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => "Command", has => [ nsname => { shell_args_position => 1, doc => 'The name of the namespace, and first "word" in all class names', }, ], doc => 'create a new namespace tree and top-level module', ); sub help_brief { "Used to define a new Namespace as part of starting a new project." } sub sub_command_sort_position { 1 } our $module_template=<nsname; if (-e $name . ".pm") { $self->error_message("Module ${name}.pm already exists!"); return; } eval "package $name;"; if ($@) { $self->error_message("Invalid package name $name: $@"); return; } # Step 1 - Make a new Namespace my $namespace = UR::Object::Type->define(class_name => $name, is => ['UR::Namespace'], is_abstract => 0); my $namespace_src = $namespace->resolve_module_header_source; # Step 2 - Make an empty Vocabulary my $vocab_name = $name->get_vocabulary(); my $vocab = UR::Object::Type->define( class_name => $vocab_name, is => 'UR::Vocabulary', is_abstract => 0, ); my $vocab_src = $vocab->resolve_module_header_source(); my $vocab_filename = $vocab->module_base_name(); # write the namespace module $self->status_message("A $name (UR::Namespace)\n"); IO::File->new("$name.pm", 'w')->printf($module_template, $name, $namespace_src); # Write the vocbaulary module mkdir($name); IO::File->new($vocab_filename,'w')->printf($module_template, $vocab_name, $vocab_src); $self->status_message("A $vocab_name (UR::Vocabulary)\n"); # Step 3 - Make and write a new Meta DataSource module # and also, the SQL source for a new, empty metadata DB my ($meta_datasource, $meta_db_file) = UR::DataSource::Meta->generate_for_namespace($name); my $meta_datasource_name = $meta_datasource->id; $self->status_message("A $meta_datasource_name (UR::DataSource::Meta)\n"); $self->status_message("A $meta_db_file (Metadata DB skeleton)"); return 1; } 1; Datasource.pm000444023532023421 344112121654175 22014 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define # The diff command delegates to sub-commands under the adjoining directory. package UR::Namespace::Command::Define::Datasource; use warnings; use strict; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Base", has_optional => [ dsid => { is => 'Text', doc => "The full class name to give this data source.", }, dsname => { is => 'Text', shell_args_position => 1, doc => "The distinctive part of the class name for this data source. Will be prefixed with the namespace then '::DataSource::'.", }, ], doc => 'add a data source to the current namespace' ); sub _is_hidden_in_docs { 1 } sub sub_command_sort_position { 2 } sub data_source_module_pathname { my $self = shift; my $ns_path = $self->namespace_path; my $dsid = $self->dsid; my @ds_parts = split(/::/, $dsid); shift @ds_parts; # Get rid of the namespace name my $filename = pop @ds_parts; $filename .= '.pm'; my $path = join('/', $ns_path, @ds_parts, $filename); return $path; } # Overriding these so one can be calculated from the other sub dsid { my $self = shift; my $dsid = $self->__dsid; unless ($dsid) { my $dsname = $self->__dsname; my $namespace = $self->namespace_name; $dsid = $namespace . '::DataSource::' . $dsname; $self->__dsid($dsid); } return $dsid; } sub dsname { my $self = shift; my $dsname = $self->__dsname; unless ($dsname) { my $dsid = $self->__dsid; # assumme the name is the last portion of the class name $dsname = (split(/::/,$dsid))[-1]; $self->__dsname($dsname); } return $dsname; } 1; Datasource000755023532023421 012121654175 21317 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/DefinePg.pm000444023532023421 63012121654173 22335 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Pg; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", ); sub help_brief { "Add a PostgreSQL data source to the current namespace." } sub _data_source_sub_class_name { 'UR::DataSource::Pg' } 1; Oracle.pm000444023532023421 62112121654173 23174 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Oracle; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", doc => "Add an Oracle data source to the current namespace." ); sub _data_source_sub_class_name { 'UR::DataSource::Oracle' } 1; Rdbms.pm000444023532023421 1002612121654174 23077 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Rdbms; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Define::Datasource', has => [ server => { is => 'String', doc => '"server" attribute for this data source, such as a database name', is_optional => 1, }, nosingleton => { is => 'Boolean', doc => 'Created data source should not inherit from UR::Singleton (defalt is that it will)', default_value => 0, }, ], is_abstract => 1, ); sub help_description { "Define a UR datasource connected to a relational database through UR::DataSource::RDBMS and DBI"; } sub execute { my $self = shift; my $namespace = $self->namespace_name; unless ($namespace) { $self->error_message("This command must be run from a namespace directory."); return; } unless ($self->__dsname || $self->__dsid) { $self->error_message("Either --dsname or --dsid is required"); return; } # Force an autoload of the namespace module #my $ret = above::use_package($namespace); eval "use $namespace"; if ($@) { $self->error_message("Can't load namespace $namespace: $@"); return; } unless (defined $self->server) { $self->server($self->dsname); } my $ds_id = $self->dsid; my $c = eval { UR::DataSource->get($ds_id) || $ds_id->get() }; if ($c) { $self->error_message("A data source named $ds_id already exists\n"); return; } my $src = $self->_resolve_module_header($ds_id,$namespace); my($class_definition,$parent_classes) = $self->_resolve_class_definition_source(); $src .= $class_definition; my $module_body = $self->_resolve_module_body(); $src .= "\n$module_body\n1;\n"; my $module_path = $self->data_source_module_pathname(); my $fh = IO::File->new($module_path, O_WRONLY | O_CREAT | O_EXCL); unless ($fh) { $self->error_message("Can't open $module_path for writing: $!"); return; } $fh->print($src); $fh->close(); $self->status_message("A $ds_id (" . join(',', @$parent_classes) . ")\n"); $self->_post_module_written(); if ($self->_try_connect()) { return 1; } else { return; } } sub _resolve_module_header { my($self,$ds_id, $namespace) = @_; return "package $ds_id;\n\nuse strict;\nuse warnings;\n\nuse $namespace;\n\n"; } # Subclasses can override this to have something happen after the module # is written, but before we try connecting to the DS sub _post_module_written { return 1; } # Subclasses must override this to indicate what abstract DS class they should # inherit from sub _data_source_sub_class_name { my $self = shift; my $class = ref($self); die "Class $class didn't implement _data_source_sub_class_name"; } sub _resolve_class_definition_source { my $self = shift; my $ds_id = $self->dsid; my $parent_ds_class = $self->_data_source_sub_class_name(); my $src = "class $ds_id {\n"; my @parent_classes = ( $parent_ds_class ); if (! $self->nosingleton) { push @parent_classes, 'UR::Singleton'; } $src .= sprintf(" is => [ '%s' ],\n", join("', '", @parent_classes)); $src .= "};\n"; return($src,\@parent_classes); } sub _resolve_module_body { my $self = shift; my $server = $self->server; my $src = "sub server { '$server' }\n"; return $src; } sub _try_connect { my $self = shift; $self->status_message(" ...connecting..."); my $ds_id = $self->dsid; my $dbh = $ds_id->get_default_handle(); if ($dbh) { $self->status_message(" ....ok\n"); return 1; } else { $self->error_message(" ERROR: " . $ds_id->error_message); return; } } 1; Mysql.pm000444023532023421 63112121654174 23076 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Mysql; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::RdbmsWithAuth", ); sub help_brief { "Add a MySQL data source to the current namespace." } sub _data_source_sub_class_name { 'UR::DataSource::MySQL' } 1; Sqlite.pm000444023532023421 275212121654174 23260 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::Sqlite; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => [ 'UR::Namespace::Command::Define::Datasource::Rdbms' ], ); sub help_brief { "Add a SQLite data source to the current namespace." } sub help_synopsis { return <super_can('server'); if (@_) { # unusual case, setting the server return $super_server($self,@_); } my $server = $super_server->($self); unless ($server) { $server = $self->data_source_module_pathname(); $server =~ s/\.pm$/.sqlite3/; $super_server->($self,$server); } return $server; } sub _post_module_written { my $self = shift; # Create a new, empty DB if it dosen't exist yet my $pathname = $self->server; $pathname =~ s/\.pm$/.sqlite3/; IO::File->new($pathname, O_WRONLY | O_CREAT) unless (-f $pathname); $self->status_message("A $pathname (empty database schame)"); return 1; } 1; RdbmsWithAuth.pm000444023532023421 174412121654175 24545 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::RdbmsWithAuth; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::Define::Datasource::Rdbms", has => [ login => { is => 'String', doc => 'User to log in with', }, auth => { is => 'String', doc => 'Password to log in with', }, owner => { is => 'String', doc => 'Owner/schema to connect to', }, ], is_abstract => 1, ); sub _resolve_module_body { my $self = shift; my $src = $self->SUPER::_resolve_module_body(@_); my $login = $self->login; $src .= "sub login { '$login' }\n"; my $auth = $self->auth; $src .= "sub auth { '$auth' }\n"; my $owner = $self->owner; $src .= "sub owner { '$owner' }\n"; return $src; } 1; File.pm000444023532023421 173512121654175 22677 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Define/Datasourcepackage UR::Namespace::Command::Define::Datasource::File; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use IO::File; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Define::Datasource', has => [ server => { is => 'String', doc => '"server" attribute for this data source, such as a database name', }, singleton => { is => 'Boolean', default_value => 1, doc => 'by default all data sources are singletons, but this can be turned off' }, ], doc => 'Add a file-based data source (not yet implemented)' ); sub help_description { "Define a UR datasource connected to a file"; } sub execute { my $self = shift; $self->warning_message("This command is not yet implemented. See the documentation for UR::DataSource::File for more information about creating file-based data sources"); return; } 1; List000755023532023421 012121654175 16746 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandClasses.pm000444023532023421 65212121654173 21017 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/List package UR::Namespace::Command::List::Classes; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_description { "List all classes in the current namespace." } sub for_each_class_object { my $self = shift; my $class = shift; print $class->class_name,"\n"; } 1; Modules.pm000444023532023421 63612121654174 21035 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/List package UR::Namespace::Command::List::Modules; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => "UR::Namespace::Command::RunsOnModulesInTree", ); sub help_description { "List all modules in the current namespace." } sub for_each_module_file { my $self = shift; my $module = shift; print "$module\n"; } 1; Objects.pm000444023532023421 65212121654175 21015 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Listpackage UR::Namespace::Command::List::Objects; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use above "UR"; use UR::Object::Command::List; class UR::Namespace::Command::List::Objects { is => 'UR::Object::Command::List', }; 1; #$HeadURL: svn+ssh://svn/srv/svn/gscpan/distro/ur-bundle/trunk/lib/UR/Namespace/Command/List/Objects.pm $ #$Id: Objects.pm 36327 2008-07-08 20:59:29Z ebelter $ Sys000755023532023421 012121654173 16607 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/CommandClassBrowser.pm000444023532023421 322612121654173 21716 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/Command/Syspackage UR::Namespace::Command::Sys::ClassBrowser; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Namespace::Command::Base', ); # This turns on the perl stuff to insert data in the DB # namespace so we can get line numbers and stuff about # loaded modules BEGIN { unless ($^P) { no strict 'refs'; *DB::DB = sub {}; $^P = 0x31f; } } sub is_sub_command_delegator { 0;} sub help_brief { "Start a web server to browse through the class and database structures."; } sub execute { my $self = shift; my $params = shift; my $namespace = $self->namespace_name; # FIXME why dosen't require work here? eval "use $namespace"; if ($@) { $self->error_message("Failed to load module for $namespace: $@"); return; } # FIXME This is a hack to preload all the default namespace's classes at startup # when the class metadata is in the SQLite DB, this won't be necessary anymore print "Preloading class information for namespace $namespace\n"; $namespace->get_material_class_names; # FIXME the vocabulary converted "cgi app" into CgiApp, instead of CGIApp even though # I added CGI to the list of special cased words in GSC::Vocabulary. It looks like # UR::Object::View::create() is hard coded to use App::Vocabulary instead of whatever # the current namespace's vocabulary is my $v = $namespace->create_view(perspective => "schema browser", toolkit => "cgi app"); printf("URL is http://%s:%d/\n",$v->hostname, $v->port); $v->timeout(600); $v->show(); } 1; View000755023532023421 012121654172 15364 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/NamespaceSchemaBrowser000755023532023421 012121654174 20132 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/ViewCgiApp.pm000444023532023421 1175212121654174 22016 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowserpackage UR::Namespace::View::SchemaBrowser::CgiApp; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Namespace::View::SchemaBrowser::CgiApp', is => 'UR::Object::View', properties => [ http_server => {}, port => { type => 'integer' }, hostname => { type => 'string' }, data_dir => { type => 'string' }, timeout => { type => 'integer' }, ], ); use File::Temp; use Sys::Hostname qw(); use Net::HTTPServer; use Class::Autouse \&dynamically_load_page_class; sub create { my($class, %params) = @_; ##$DB::single = 1; my $port = delete $params{'port'}; my $data_dir = delete $params{'data_dir'}; my $sessions = delete $params{'sessions'}; my $server_type = delete $params{'server_type'}; my $timeout = delete $params{'timeout'}; my $self = $class->SUPER::create(%params); $data_dir ||= File::Temp::tempdir('schemabrowserXXXXX', CLEANUP => 1); my $server = Net::HTTPServer->new( chroot => 1, datadir => $data_dir, docroot => undef, index => 'index.html', ssl => 0, port => $port || 'scan', sessions => $sessions || 0, type => $server_type || 'single', ); unless ($server) { $self->error_message("Can't create HTTPServer object: $!"); return; } $server->RegisterRegex('.*', sub { $self->render_page(@_) }); $port = $server->Start(); unless ($port) { $self->error_message("HTTPServer couldn't start: $!"); return; } $self->port($port); $self->data_dir($data_dir); $self->hostname(Sys::Hostname::hostname()); $self->timeout($timeout); $self->http_server($server); return $self; } sub show { my $self = shift; #$DB::single = 1; my $server = $self->http_server; my $timeout = $self->timeout; our $LAST_PAGE_TIME = time(); while($server->Process($timeout)) { last if ((time() - $LAST_PAGE_TIME) > $self->timeout); } $server->Stop(); return 1; } sub render_page { my $self = shift; my $req = shift; #$DB::single = 1; my $resp = $req->Response; our $LAST_PAGE_TIME = time; my($page) = ($req->Path =~ m/\/?(.*)\.html$/); $page ||= 'Index'; $page = ucfirst $page; my $page_class = $self->__meta__->class_name . '::' . $page; our %PAGE_OBJ_CACHE; my $page_obj = $PAGE_OBJ_CACHE{$page_class} ||= eval { $page_class->new(ur_namespace => $self->subject_class_name) }; if (!$page_obj and $@) { print "Exception when calling new() on $page_class: $@\n"; $resp->Print("Exception when calling new() on $page_class: $@\n"); $resp->Code(500); return $resp; } my $output; if ($page_obj) { $page_obj->request($req); $page_obj->response($resp); $output = eval { $page_obj->run() }; if (!$output and $@) { my $error = $@; $error =~ s/\n/
/; $output = "Exception when calling run() on an instance of $page_class: $@"; $resp->Code(500); } else { $resp->Code(200); } } else { $output = q(Object not found

Object not found!

The URL you requested could not be translated to a valid module); $resp->Code(404); } $resp->Print($output); return $resp; } # The classes that implement each page aren't UR-based classes, so we # handle the autloading and subclassing of the namespace's page classes # here sub dynamically_load_page_class { my($class_name, $method_name) = @_; #$DB::single = 1; my @parts = split(/::/, $class_name); for (my $idx = @parts; $idx >= 0; $idx--) { my $parent_class = join('::',@parts[0 .. $idx-1]); my $page_class = join('::',@parts[$idx .. $#parts]); my $class_obj = eval {UR::Object::Type->get(class_name => $parent_class) }; next unless $class_obj; if (grep {$_ eq __PACKAGE__} $class_obj->ancestry_class_names) { my $isa_name = $parent_class . '::' . $page_class . '::ISA'; my $schemabrowser_class_name = __PACKAGE__ . '::' . $page_class; no strict 'refs'; push @{$isa_name}, $schemabrowser_class_name; # FIXME why dosen't require work here? eval "use $schemabrowser_class_name"; last; } } no warnings; my $ref = $class_name->can($method_name); } 1; =pod =head1 NAME UR::Namespace::View::SchemaBrowser::CgiApp - View class for metadata via the browser namespace command =head1 DESCRIPTION This class implements the view behavior behind the metadata web browser. =head1 SEE ALSO UR::Namespace::Command::Browser, 'ur browser --help' =cut CgiApp000755023532023421 012121654175 21276 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowserIndex.pm000444023532023421 61412121654172 23016 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowser/CgiApppackage UR::Namespace::View::SchemaBrowser::CgiApp::Index; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use base 'UR::Namespace::View::SchemaBrowser::CgiApp::Base'; sub _template{q( Class/Schema Browser Browse the Schema
Browse the Classes )}; 1; Schema.pm000444023532023421 2227312121654172 23214 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowser/CgiApppackage UR::Namespace::View::SchemaBrowser::CgiApp::Schema; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use base 'UR::Namespace::View::SchemaBrowser::CgiApp::Base'; use Class::Inspector; sub setup { my($self) = @_; $self->start_mode('show_schema_page'); $self->mode_param('rm'); $self->run_modes( 'show_schema_page' => 'show_schema_page', ); } sub show_schema_page { my($self) = @_; #$DB::single = 1; my @namespace_names = $self->GetNamespaceNames(); my $namespace_name = $self->namespace_name; $self->tmpl->param(SELECTED_NAMESPACE => $namespace_name); $self->tmpl->param(NAMESPACE_NAMES => [ map { { NAMESPACE_NAME => $_, SELECTED => ($_ eq $namespace_name), } } @namespace_names ]); my $selected_table = $self->request->Env('tablename') || ''; return $self->tmpl->output() unless ($namespace_name); my $namespace = UR::Namespace->get($namespace_name); my @data_sources = $namespace->get_data_sources(); my @table_names = map { $_->table_name } UR::DataSource::RDBMS::Table->get(data_source => \@data_sources); $self->tmpl->param(SELECTED_TABLE => $selected_table); $self->tmpl->param(TABLE_NAMES => [ map { { TABLENAME => $_, SELECTED => ($_ eq $selected_table), LINK_PARAMS => join('&', "namespace=$namespace_name", "tablename=$_"), URL => 'schema.html', } } @table_names ]); return $self->tmpl->output() unless ($selected_table && grep { $_ eq $selected_table} @table_names); # FIXME This won't work if there are tables of the same name in different data sources... my $tableobj = UR::DataSource::RDBMS::Table->get(data_source => \@data_sources, table_name => $selected_table); # FIXME There's a a "bug" in getting class objects by attributes other than the class name. There's a workaround # in the code that lets it work if you also pass in the namespace. my $class_name_for_table = $tableobj->handler_class_name(namespace => $namespace_name); $self->tmpl->param(SELECTED_TABLE_CLASS => $class_name_for_table); $self->tmpl->param(SELECTED_TABLE_DATASOURCE => $tableobj->data_source); my @table_detail; my %primary_keys = map { $_ => 1 } $tableobj->primary_key_constraint_column_names; foreach my $column_obj ( $tableobj->columns() ) { my %properties; #my @properties = split(/\s/, $colinfo); #my $colname = $properties[0]; my $property_obj = UR::Object::Property->get(class_name => $class_name_for_table, column_name => $column_obj->column_name); $properties{'COLUMN'} = $column_obj->column_name; $properties{'ACCESSOR'} = $property_obj->property_name; $properties{'TYPE'} = sprintf('%s(%d)',$column_obj->data_type || '', $column_obj->data_length || 0); $properties{'CONSTRAINTS'} = sprintf('%s %s', $primary_keys{$column_obj->column_name} ? 'PK' : '', $column_obj->nullable eq 'N' ? 'NOT NULL' : ''); $properties{'REMARKS'} = $column_obj->remarks; # FIXME do any FKs have multiple originating or reference column names? if (my @fk_names = $column_obj->fk_constraint_names) { $properties{'FK'} = [ map { { R_TABLE => $_->r_table_name, R_COL => $_->r_column_name, NAMESPACE => $namespace_name, }} UR::DataSource::RDBMS::FkConstraintColumn->get(fk_constraint_name => \@fk_names, table_name => $selected_table, column_name => $column_obj->column_name) ]; } else { $properties{'FK'} = []; } push @table_detail, \%properties; } $self->tmpl->param(SELECTED_TABLE_DETAIL => [ sort {$a->{'COLUMN'} cmp $b->{'COLUMN'}} @table_detail ]); my @ref_fk_info = map { { NAMESPACE => $namespace_name, FK_TABLE => $_, }} sort map {$_->table_name()} $tableobj->ref_fk_constraints(); $self->tmpl->param(REFERRING_TABLES => \@ref_fk_info); return $self->tmpl->output(); } sub _template { q( Database Schema<TMPL_IF NAME="SELECTED_TABLE">: <TMPL_VAR NAME="SELECTED_TABLE"></TMPL_IF>
Namespace:
Table:
?">

&classname=">Class No related class
Data Source:
ColumnAccessorTypeConstraintsForeign Key To
      &tablename=">.
Referring tables
&tablename=">
)}; 1; Class.pm000444023532023421 4254712121654173 23070 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowser/CgiApppackage UR::Namespace::View::SchemaBrowser::CgiApp::Class; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use base 'UR::Namespace::View::SchemaBrowser::CgiApp::Base'; use Class::Inspector; sub setup { my($self) = @_; #$DB::single = 1; $self->start_mode('show_class_page'); $self->mode_param('rm'); $self->run_modes( 'show_class_page' => 'show_class_page', ); } sub show_class_page { my $self = shift; #$DB::single = 1; my @namespace_names = $self->GetNamespaceNames(); my $namespace_name = $self->namespace_name; $self->tmpl->param(SELECTED_NAMESPACE => $namespace_name); $self->tmpl->param(NAMESPACE_NAMES => [ map { { NAMESPACE_NAME => $_, SELECTED => ($_ eq $namespace_name), } } @namespace_names ]); return $self->tmpl->output() unless ($namespace_name); my $namespace = UR::Namespace->get($namespace_name); my $class_name = $self->request->Env('classname') || ''; my @all_class_names = $namespace->get_material_class_names(); $self->tmpl->param(SELECTED_CLASS => $class_name); $self->tmpl->param(CLASS_NAMES => [ map { { CLASS_NAME => $_, SELECTED => ($_ eq $class_name), LINK_PARAMS => join('&', "namespace=$namespace_name", "classname=$_"), URL => 'class.html', } } @all_class_names ]); return $self->tmpl->output() unless ($class_name); my $class_obj = UR::Object::Type->get(namespace => $namespace_name, class_name => $class_name); $self->tmpl->param(SELECTED_CLASS_TABLE => defined($class_obj) && $class_obj->table_name); $self->tmpl->param(CLASS_IS_UR => $class_obj ? 1 : 0); my %class_properties; if ($class_obj) { my @class_detail; foreach my $prop_name ( qw( namespace doc er_role is_abstract is_final is_singleton sub_classification_meta_class_name subclassify_by) ) { push @class_detail, { PROPERTY_NAME => $prop_name, PROPERTY_VALUE => $class_obj->$prop_name }; } push @class_detail, { PROPERTY_NAME => 'data_source', PROPERTY_VALUE => UR::Context->resolve_data_source_for_object($class_obj)}; $self->tmpl->param(CLASS_DETAIL => \@class_detail); my @class_properties; my %id_properties = map { $_ => 1 } $class_obj->all_id_property_names; foreach my $prop_obj ( $class_obj->all_property_metas ) { next if ($prop_obj->property_name eq 'id'); # FIXME what if the 'id' property is real and not autogenerated? $class_properties{$prop_obj->property_name} = 1; push @class_properties, { PROPERTY_NAME => $prop_obj->property_name, PROPERTY_TYPE => $prop_obj->data_type, PROPERTY_LENGTH => $prop_obj->data_length, IS_ID_PROPERTY => $id_properties{$prop_obj->property_name} || 0, }; } $self->tmpl->param(CLASS_PROPERTIES => \@class_properties); } my $filename = Class::Inspector->loaded_filename($class_name); $self->tmpl->param('FILENAME' => $filename); my $method_sort_col; my $method_sorter = $self->request->Env('method_sorter') || 'method'; if ($method_sorter eq 'class') { $method_sort_col = 1; # Sort by what class the method is defined in $self->tmpl->param(SORT_METHODS_BY_CLASS => 1); $self->tmpl->param(SORT_METHODS_BY_NAME => 0); } else { $method_sort_col = 2; # Sort by the method name $self->tmpl->param(SORT_METHODS_BY_NAME => 1); $self->tmpl->param(SORT_METHODS_BY_CLASS => 0); } $self->tmpl->param('CLASS_INHERIT' => $self->_MakeClassInheritance($class_name)); my $pub_method_list = Class::Inspector->methods($class_name, 'public','expanded'); my $priv_method_list = Class::Inspector->methods($class_name,'private','expanded'); $self->tmpl->param('CLASS_PUBLIC_METHODS' => [ map { { CLASS_NAME => $_->[1], METHOD_NAME => $_->[2], NAMESPACE => $namespace_name, OVERRIDES => [ $self->GetMethodOverrides($_->[1], $_->[2]) ], $self->GetMethodLocation($_), }} sort {$a->[$method_sort_col] cmp $b->[$method_sort_col]} @$pub_method_list ]); $self->tmpl->param('CLASS_PRIVATE_METHODS' => [ map { { CLASS_NAME => $_->[1], METHOD_NAME => $_->[2], NAMESPACE => $namespace_name, OVERRIDES => [ $self->GetMethodOverrides($_->[1], $_->[2]) ], $self->GetMethodLocation($_), }} sort {$a->[$method_sort_col] cmp $b->[$method_sort_col]} @$priv_method_list ]); return $self->tmpl->output(); } # Given a listref that would be returned as one item of Class::Inspector->methods # ['Class::method1','Class','method1',\&Class::method1] # Return a hash with keys FILENAME => pathanme of file the class is in # and LINENO => line in the file that this method is defined in # This requires that the debugger flags $^P were turned on before the # module was loaded sub GetMethodLocation { my($self,$methodinfo) = @_; my $name = $methodinfo->[0]; my $info = $DB::sub{$name}; return () unless $info; my ($file,$start,$end); if ($info =~ m/\[(.*?):(\d+)\]/) { # This should match eval's and __ANON__s ($file,$start,$end) = ($1,$2,$2); } elsif ($info =~ m/(.*?):(\d+)-(\d+)$/) { ($file,$start,$end) = ($1,$2,$3); } return (FILENAME => $file, LINENO => $start); } # Given a listref that would be returned as one item of Class::Inspector->methods # ['Class::method1','Class','method1',\&Class::method1] # Return a hash with keys CLASS_NAME => class_name as a result of searching the # inheritance hirarchy for other classes where this method name was defined # and therfore overridden sub GetMethodOverrides { my($self,$class_name,$method) = @_; my @results; my %seen; my @isa = ($class_name); while (@isa) { my $superclass = shift @isa; next if $seen{$superclass}; $seen{$superclass} = 1; if (Class::Inspector->function_exists($superclass, $method)) { push @results, { CLASS_NAME => $superclass }; } { no strict 'vars'; push @isa, eval '@' . $class_name . '::ISA'; } } shift @results; # Throw out the first one. It'll be reported as the real method call return @results; } sub _MakeClassInheritance { my($self,$starting_class_name) = @_; my $recurse_sub; my $maxdepth = 0; my @retval = (); my $namespace = $self->request->Env('namespace'); $recurse_sub = sub { my($class_name,$depth) = @_; $maxdepth = $depth if ($depth > $maxdepth); my @isa_list; { no strict 'refs'; @isa_list = @{"${class_name}::ISA"}; } return () unless @isa_list; unshift(@retval, { DEPTH => $depth, NAMESPACE => $namespace, CLASS_NAME => $class_name, }); foreach my $subclass ( @isa_list ) { $recurse_sub->($subclass, $depth + 1); } }; $recurse_sub->($starting_class_name, 1); # Alter the 'depth' value at each node so the base class becomes # depth 1, and the original class is the deepest $maxdepth--; foreach my $node ( @retval ) { $node->{'DEPTH'} = $maxdepth - $node->{'DEPTH'}; $node->{'DEPTH_L'} = [ 1 .. $node->{'DEPTH'} ]; } return \@retval; } sub _template{ q( Class Browser<TMPL_IF NAME="SELECTED_CLASS">: <TMPL_VAR NAME="SELECTED_CLASS"></TMPL_IF>
Namespace:
Class name:
?">

&tablename=">Table No related table

Loaded from file "> No related module file


Class Interitance
Class Metadata Information

Class Properties
Property Name Data Type Data Length

Public Methods &classname=&method_sorter=method">Public Methods Interited From Inherited from
#line"> &classname=">

Private Methods
#line"> &classname=">

Please select a class on the left
)}; 1; Base.pm000444023532023421 437112121654174 22647 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowser/CgiApppackage UR::Namespace::View::SchemaBrowser::CgiApp::Base; # Since the classes that implement web pages have to be a subclass of # CGI::Application, these are not UR-based classes, but more traditional # Perl classes use CGI; use base 'CGI::Application'; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; sub new { my $class = shift; my %params = @_; #$DB::single = 1; my $ur_namespace = delete $params{'ur_namespace'}; my $self = $class->SUPER::new(%params); my $template_data = $self->_template(); if ($template_data) { my $tmpl = $self->load_tmpl(\$template_data,die_on_bad_params => 0, cache => 0); $tmpl->param('CLASS' => $class); $self->tmpl($tmpl); } $self->run_modes('start' => '_default_render'); $self->header_type('none'); $self->ur_namespace($ur_namespace); return $self; } sub cgiapp_get_query { my $self = shift; my $cgi = CGI->new($self->request->Query()); return $cgi; } # create the basic accessors our $PACKAGE = __PACKAGE__; foreach my $acc_name ( 'request','response','tmpl', 'ur_namespace' ) { my $subref = sub { my $self = shift; if (@_) { $self->{$PACKAGE}->{$acc_name} = shift; } else { $self->{$PACKAGE}->{$acc_name}; } }; no strict 'refs'; *{$acc_name} = $subref; } sub namespace_name { my $self = shift; return $self->request->Env('namespace') || $self->ur_namespace || ''; } sub _default_render { my $self = shift; #$DB::single = 1; $self->tmpl->output(); } sub run { my $self = shift; my $buffer = ""; my $fh; open ($fh, '>', \$buffer); my $old_fh = select $fh; my $output = $self->SUPER::run(@_); select $old_fh; return $buffer; } # FIXME is there a way to dynamically get all the available namespaces? sub GetNamespaceNames { return map { $_->class } UR::Namespace->is_loaded(); } sub _template { q( Default Page

You didn't specify a DATA section for class

)}; 1; File.pm000444023532023421 222212121654175 22646 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Namespace/View/SchemaBrowser/CgiApppackage UR::Namespace::View::SchemaBrowser::CgiApp::File; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use base 'UR::Namespace::View::SchemaBrowser::CgiApp::Base'; use IO::File; sub setup { my($self) = @_; #$DB::single = 1; $self->start_mode('show_file'); $self->mode_param('rm'); $self->run_modes( 'show_file' => 'show_file', ); } sub show_file { my $self = shift; my $file = $self->request->Env('filename'); my $linenum = $self->request->Env('linenum'); my $fh = IO::File->new($file); unless ( $fh ) { return "File Browse ErrorCan't open file $file: $!"; } my @data; my $lineno = 1; foreach my $line ( $fh->getlines() ) { chomp $line; push @data, { LINE => 'line'.$lineno++, DATA => $line }; } $self->tmpl->param(FILE_LINES => \@data); $self->tmpl->output(); } sub _template{ q( File view
">

)}; 1; BoolExpr000755023532023421 012121654175 14313 5ustar00abrummetgsc000000000000UR-0.41/lib/URUtil.pm000444023532023421 775212121654173 15734 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr package UR::BoolExpr::Util; # Non-OO Utility methods for the rule modules. use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use Scalar::Util qw(blessed); use Data::Dumper; use FreezeThaw; # Because the id is actually a full data structure we need some separators. # Note that these are used for the common case, where FreezeThaw is for arbitrarily complicated rule identifiers. our $id_sep = chr(29); # spearetes id property values instead of the old \t our $record_sep = chr(30); # within a value_id, delimits a distinct values our $unit_sep = chr(31); # seperates items within a single value our $null_value = chr(21); # used for undef/null our $empty_string = chr(28); # used for "" our $empty_list = chr(20); # used for [] # These are used when there is any sort of complicated data in the rule. sub values_to_value_id_frozen { my $self = shift; my $frozen = FreezeThaw::safeFreeze(@_); return "F:" . $frozen; } sub value_id_to_values_frozen { my $self = shift; my $value_id = shift; return FreezeThaw::thaw($value_id); } # These are used for the simple common-case rules. sub values_to_value_id { my $self = shift; my $value_id = "O:"; for my $value (@_) { no warnings;# 'uninitialized'; if (length($value)) { if (ref($value) eq "ARRAY") { if (@$value == 0) { $value_id .= $empty_list; } else { for my $value2 (@$value) { if (not defined $value2 ) { $value_id .= $null_value . $unit_sep; } elsif ($value2 eq "") { $value_id .= $empty_string . $unit_sep; } else { if (ref($value2) or index($value2, $unit_sep) >= 0 or index($value2, $record_sep) >= 0) { return $self->values_to_value_id_frozen(@_); } $value_id .= $value2 . $unit_sep; } } } $value_id .= $record_sep; } else { if (ref($value) or index($value,$unit_sep) >= 0 or index($value,$record_sep) >= 0) { return $self->values_to_value_id_frozen(@_); } $value_id .= $value . $record_sep; } } elsif (not defined $value ) { $value_id .= $null_value . $record_sep; } else {# ($value eq "") { $value_id .= $empty_string . $record_sep; } } return $value_id; } sub value_id_to_values { my $self = shift; my $value_id = shift; unless (defined $value_id) { Carp::confess('No value_id passed in to value_id_to_values()!?'); } my $method_identifier = substr($value_id,0,2); $value_id = substr($value_id, 2, length($value_id)-2); if ($method_identifier eq "F:") { return $self->value_id_to_values_frozen($value_id); } my @values = ($value_id =~ /(.*?)$record_sep/gs); for (@values) { if (substr($_,-1) eq $unit_sep) { #$_ = [split($unit_sep,$_)] my @values2 = /(.*?)$unit_sep/gs; $_ = \@values2; for (@values2) { if ($_ eq $null_value) { $_ = undef; } elsif ($_ eq $empty_string) { $_ = ""; } } } elsif ($_ eq $null_value) { $_ = undef; } elsif ($_ eq $empty_string) { $_ = ""; } elsif ($_ eq $empty_list) { $_ = []; } } return @values; } *values_to_value_id_simple = \&values_to_value_id; 1; =pod =head1 NAME UR::BoolExpr::Util - non-OO module to collect utility functions used by the BoolExpr modules =cut BxParser.yp000444023532023421 3330212121654173 16567 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr%right OR %right AND %left MINUS # Generate the perl module with the command: # yapp -sm UR::BoolExpr::BxParser -o - BxParser.yp | sed s/Parse::Yapp::Driver/UR::BoolExpr::BxParser::Yapp::Driver/ > BxParser.pm %% boolexpr: { [] } | expr { UR::BoolExpr::BxParser->_simplify($_[1]) } | boolexpr ORDER_BY order_by_list { [@{$_[1]}, '-order', $_[3]] } | boolexpr GROUP_BY group_by_list { [@{$_[1]}, '-group', $_[3]] } | boolexpr LIMIT INTEGER { [@{$_[1]}, '-limit', $_[3]] } | boolexpr OFFSET INTEGER { [@{$_[1]}, '-offset', $_[3]] } ; expr: condition { $_[1] } | expr AND expr { UR::BoolExpr::BxParser->_and($_[1], $_[3]) } | expr OR expr { UR::BoolExpr::BxParser->_or($_[1], $_[3]) } | LEFT_PAREN expr RIGHT_PAREN { $_[2] } ; condition: property operator optional_spaces value { [ "$_[1] $_[2]" => $_[4] ] } | property like_operator like_value { [ "$_[1] $_[2]" => $_[3] ] } | property in_operator set { [ "$_[1] $_[2]" => $_[3] ] } | property COLON optional_spaces old_syntax_in_value { [ "$_[1] in" => $_[4] ] } | property negation COLON optional_spaces old_syntax_in_value { [ "$_[1] $_[2] in" => $_[5] ] } | property between_operator between_value { [ "$_[1] $_[2]" => $_[3] ] } | property COLON optional_spaces between_value { [ "$_[1] between" => $_[4] ] } | property negation COLON optional_spaces between_value { [ "$_[1] $_[2] between" => $_[5] ] } | property boolean_op_word { [ "$_[1] $_[2]" => 1 ] } | property null_op_word { [ "$_[1] $_[2]" => undef ] } ; boolean_op_word: TRUE_WORD { $_[1] } | FALSE_WORD { $_[1] } ; null_op_word: IS_NULL { '=' } | negation IS_NULL { "!=" } | IS_NOT_NULL { '!=' } ; spaces: WHITESPACE { $_[1] } ; optional_spaces: { undef } | spaces { undef } ; property: IDENTIFIER { $_[1] } | keyword_as_value { $_[1] } ; order_by_property: property { $_[1 ] } | MINUS property { '-'.$_[2] } | property DESC_WORD { '-'.$_[1] } | property ASC_WORD { $_[1] } ; order_by_list: order_by_property { [ $_[1]] } | order_by_property AND order_by_list { [$_[1], @{$_[3]}] } ; group_by_list: property { [ $_[1] ] } | property AND group_by_list { [$_[1], @{$_[3]}] } ; operator: an_operator { $_[1] } | negation an_operator { "$_[1] $_[2]" } ; negation: NOT_WORD { 'not' } | NOT_BANG { 'not' } ; an_operator: OPERATORS { $_[1] } | EQUAL_SIGN { '=' } | DOUBLEEQUAL_SIGN { '=' } ; like_operator: LIKE_WORD { 'like' } | negation LIKE_WORD { "$_[1] like" } | TILDE { 'like' } | negation TILDE { "$_[1] like" } ; like_value: value { $_[1] =~ m/\%/ ? $_[1] : '%' . $_[1] . '%' } ; in_operator: IN_WORD { 'in' } | negation IN_WORD { "$_[1] in" } ; old_syntax_in_value: single_value IN_DIVIDER old_syntax_in_value { [ $_[1], @{$_[3]} ] } | single_value IN_DIVIDER single_value { [ $_[1], $_[3] ] } ; set: LEFT_BRACKET set_body RIGHT_BRACKET { $_[2] } ; set_body: value SET_SEPARATOR set_body { [ $_[1], @{$_[3]} ] } | value { [ $_[1] ] } ; between_operator: BETWEEN_WORD { 'between' } | negation BETWEEN_WORD { "$_[1] between" } ; between_value: single_value MINUS single_value { [ $_[1], $_[3] ] } ; keyword_as_value: IN_WORD { $_[1] } | LIKE_WORD { $_[1] } | BETWEEN_WORD { $_[1] } | NOT_WORD { $_[1] } | DESC_WORD { $_[1] } | ASC_WORD { $_[1] } | TRUE_WORD { $_[1] } | FALSE_WORD { $_[1] } ; value: single_value subsequent_values_list { $_[1].$_[2] } | single_value { $_[1] } ; subsequent_value_part: IDENTIFIER { $_[1] } | number { $_[1] } | WORD { $_[1] } | DOUBLEQUOTE_STRING { ($_[1] =~ m/^"(.*?)"$/)[0]; } | SINGLEQUOTE_STRING { ($_[1] =~ m/^'(.*?)'$/)[0]; } | keyword_as_value { $_[1] } ; subsequent_values_list: subsequent_value_part { $_[1] } | subsequent_value_part subsequent_values_list { $_[1].$_[2] } | spaces subsequent_values_list { $_[1].$_[2] } | spaces { '' } # to gobble the final space in a value before the next expression part ; single_value: subsequent_value_part { $_[1] } | AND { $_[1] } | OR { $_[1] } ; number: INTEGER { $_[1] + 0 } | REAL { $_[1] + 0 } | MINUS INTEGER { 0 - $_[2] } # to reject --5 | MINUS REAL { 0 - $_[2] } ; %% package UR::BoolExpr::BxParser; use strict; use warnings; sub _error { my @expect = $_[0]->YYExpect; my $tok = $_[0]->YYData->{INPUT}; my $match = $_[0]->YYData->{MATCH}; my $string = $_[0]->YYData->{STRING}; my $err = qq(Can't parse expression "$string"\n Syntax error near token $tok '$match'); my $rem = $_[0]->YYData->{REMAINING}; $err .= ", remaining text: '$rem'" if $rem; $err .= "\nExpected one of: " . join(", ", @expect) . "\n"; Carp::croak($err); } my %token_states = ( 'DEFAULT' => [ WHITESPACE => qr{\s+}, AND => [ qr{and}i, 'DEFAULT'], OR => [ qr{or}i, 'DEFAULT' ], BETWEEN_WORD => qr{between}, LIKE_WORD => qr{like}, IN_WORD => qr{in}, NOT_WORD => qr{not}, DESC_WORD => qr{desc}, ASC_WORD => qr{asc}, TRUE_WORD => qr{true}, FALSE_WORD => qr{false}, LIMIT => qr{limit}, OFFSET => qr{offset}, IDENTIFIER => qr{[a-zA-Z_][a-zA-Z0-9_.]*}, MINUS => qr{-}, INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, LEFT_PAREN => [ qr{\(}, 'DEFAULT' ], RIGHT_PAREN => [ qr{\)}, 'DEFAULT' ], LEFT_BRACKET => [ qr{\[}, 'set_contents'], RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], NOT_BANG => qr{!}, EQUAL_SIGN => [ qr{=}, 'dont_gobble_spaces' ], DOUBLEEQUAL_SIGN => [ qr{=>}, 'dont_gobble_spaces' ], OPERATORS => [ qr{<=|>=|<|>}, 'dont_gobble_spaces' ], AND => [ qr{,}, 'DEFAULT' ], COLON => [ qr{:}, 'after_colon_value' ], TILDE => qr{~}, ORDER_BY => qr{order by}, GROUP_BY => qr{group by}, IS_NULL => qr{is null|is undef}, IS_NOT_NULL => qr{is not null|is not undef}, ], 'set_contents' => [ SET_SEPARATOR => qr{,}, # Depending on state, can be either AND or SET_SEPARATOR WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], ], 'after_colon_value' => [ INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, IN_DIVIDER => qr{\/}, #WORD => qr{\w+}, # Override WORD in DEFAULT to disallow / WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # Override WORD in DEFAULT to disallow / DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, WHITESPACE => [qr{\s+}, 'DEFAULT'], ], 'dont_gobble_spaces' => [ AND => [ qr{and}, 'DEFAULT'], OR => [ qr{or}, 'DEFAULT' ], LIMIT => [qr{limit}, 'DEFAULT'], OFFSET => [qr{offset}, 'DEFAULT'], INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.\:%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards ORDER_BY => [qr{order by}, 'DEFAULT'], GROUP_BY => [qr{group by}, 'DEFAULT'], ], ); sub parse { my $string = shift; my %params = @_; my $debug = $params{'tokdebug'}; my $yydebug = $params{'yydebug'} || 0; print "\nStarting parse for string $string\n" if $debug; my $parser = UR::BoolExpr::BxParser->new(); $parser->YYData->{STRING} = $string; my $parser_state = 'DEFAULT'; my $get_next_token = sub { if (length($string) == 0) { print "String is empty, we're done!\n" if $debug; return (undef, ''); } GET_NEXT_TOKEN: foreach (1) { my $longest = 0; my $longest_token = ''; my $longest_match = ''; for my $token_list ( $parser_state, 'DEFAULT' ) { print "\nTrying tokens for state $token_list...\n" if $debug; my $tokens = $token_states{$token_list}; for(my $i = 0; $i < @$tokens; $i += 2) { my($tok, $re) = @$tokens[$i, $i+1]; print "Trying token $tok... " if $debug; my($regex,$next_parser_state); if (ref($re) eq 'ARRAY') { ($regex,$next_parser_state) = @$re; } else { $regex = $re; } if ($string =~ m/^($regex)/) { print "Matched >>$1<<" if $debug; my $match_len = length($1); if ($match_len > $longest) { print "\n ** It's now the longest" if $debug; $longest = $match_len; $longest_token = $tok; $longest_match = $1; if ($next_parser_state) { $parser_state = $next_parser_state; } } } print "\n" if $debug; } $string = substr($string, $longest); print "Consuming up to char pos $longest chars, string is now >>$string<<\n" if $debug; if ($longest_token eq 'WHITESPACE' and $parser_state ne 'dont_gobble_spaces') { print "Redoing token extraction after whitespace\n" if $debug; redo GET_NEXT_TOKEN; } $parser->YYData->{REMAINING} = $string; if ($longest) { print "Returning token $longest_token, match $longest_match\n next state is named $parser_state\n" if $debug; $parser->YYData->{INPUT} = $longest_token; $parser->YYData->{MATCH} = $longest_match; return ($longest_token, $longest_match); } last if $token_list eq 'DEFAULT'; # avoid going over it twice if $parser_state is DEFAULT } } print "Didn't match anything, done!\n" if $debug; return (undef, ''); # Didn't match anything }; return ( $parser->YYParse( yylex => $get_next_token, yyerror => \&_error, yydebug => $yydebug), \$string, ); } # Used by the top-level expr production to turn an or-type parse tree with # only a single AND condition into a simple AND-type tree (1-level arrayref). # Or to add the '-or' to the front of a real OR-type tree so it can be passed # directly to UR::BoolExpr::resolve() sub _simplify { my($class, $expr) = @_; if (ref($expr->[0])) { if (@$expr == 1) { # An or-type parse tree, but with only one AND subrule - use as a simple and-type rule $expr = $expr->[0]; } else { $expr = ['-or', $expr]; # an or-type parse tree with multiple subrules } } return $expr; } # Handles the case for "expr AND expr" where one or both exprs can be an # OR-type expr. In that case, it distributes the AND exprs among all the # OR conditions. For example: # (a=1 or b=2) and (c=3 or d=4) # is the same as # (a=1 and c=3) or (a=1 and d=4) or (b=2 and c=3) or (b=2 and d=4) # This is necessary because the BoolExpr resolver can only handle 1-level deep # AND-type rules, or a 1-level deep OR-type rule composed of any number of # 1-level deep AND-type rules sub _and { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); my @and; foreach my $left_subexpr ( @$left ) { foreach my $right_subexpr (@$right) { push @and, [@$left_subexpr, @$right_subexpr]; } } \@and; } sub _or { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); [ @$left, @$right ]; } 1; Template.pm000444023532023421 5436712121654174 16617 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr =head1 NAME UR::BoolExpr::Template - a UR::BoolExpr minus specific values =head1 SYNOPSIS =head1 DESCRIPTION =cut package UR::BoolExpr::Template; use warnings; use strict; use Scalar::Util qw(blessed); use Data::Dumper; use UR; our @CARP_NOT = qw(UR::BoolExpr); # readable stringification use overload ('""' => 'id'); use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } ); use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } ); UR::Object::Type->define( class_name => __PACKAGE__, is_transactional => 0, composite_id_separator => '/', id_by => [ subject_class_name => { is => 'Text' }, logic_type => { is => 'Text' }, logic_detail => { is => 'Text' }, constant_value_id => { is => 'Text' } ], has => [ is_normalized => { is => 'Boolean' }, is_id_only => { is => 'Boolean' }, is_partial_id => { is => 'Boolean' }, # True if at least 1, but not all the ID props are mentioned is_unique => { is => 'Boolean' }, matches_all => { is => 'Boolean' }, key_op_hash => { is => 'HASH' }, id_position => { is => 'Integer' }, normalized_id => { is => 'Text' }, normalized_positions_arrayref => { is => 'ARRAY' }, normalization_extender_arrayref => { is => 'ARRAY' }, _property_meta_hash => { is => 'HASH' }, _property_names_arrayref => { is => 'ARRAY' }, num_values => { is => 'Integer' }, _ambiguous_keys => { is => 'ARRAY' }, _keys => { is => 'ARRAY' }, _constant_values => { is => 'ARRAY' }, ], has_optional => [ hints => { is => 'ARRAY' }, recursion_desc => { is => 'ARRAY' }, order_by => { is => 'ARRAY' }, group_by => { is => 'ARRAY' }, aggregate => { is => 'ARRAY' }, limit => { is => 'Integer' }, offset => { is => 'Integer' }, ] ); our $VERSION = "0.41"; # UR $VERSION;; # Borrow from the util package. # This will go away with refactoring. our $id_sep = $UR::BoolExpr::Util::id_sep; our $record_sep = $UR::BoolExpr::Util::record_sep; our $unit_sep = $UR::BoolExpr::Util::unit_sep; our $null_value = $UR::BoolExpr::Util::null_value; our $empty_string = $UR::BoolExpr::Util::empty_string; our $empty_list = $UR::BoolExpr::Util::empty_list; # Names of the optional flags you can add to a rule our @meta_param_names = qw(recursion_desc hints order_by group_by aggregate limit offset); # Wrappers for regular properties sub _property_names { return @{ $_[0]->{_property_names_arrayref} }; } # Indexability methods sub _indexable_property_names { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; @{ $_[0]->{_indexable_property_names} } } sub _indexable_property_positions { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; @{ $_[0]->{_indexable_property_positions} } } sub _is_fully_indexable { $_[0]->_resolve_indexing_params unless $_[0]->{_resolve_indexing_params}; $_[0]->{_is_fully_indexable}; } sub _resolve_indexing_params { my $self = $_[0]; my $class_meta = UR::Object::Type->get($self->subject_class_name); my @all_names = $self->_property_names; for my $name (@all_names) { my $m = $class_meta->property($name); unless ($m) { #$DB::single = 1; $class_meta->property($name); #$DB::single = 1; $class_meta->property($name); } } my @indexable_names = sort map { $_->property_name } grep { $_ } #and $_->is_indexable } map { $class_meta->property_meta_for_name($_) } @all_names; my @indexable_positions = UR::Util::positions_of_values(\@all_names,\@indexable_names); $self->{_indexable_property_names} = \@indexable_names; $self->{_indexable_property_positions} = \@indexable_positions; $self->{_is_fully_indexable} = (@indexable_names == @all_names); return 1; } # Return true if this rule template's parameters is a subset of the other's parameters # Returns 0 if this rule specifies a parameter not in the other template # Returns undef if all the properties match, but their operators do not, meaning that # we do not know if an object evaluated as true under one rule's template would also be in the other sub is_subset_of { my($self,$other_template) = @_; my $other_template_id = $other_template->id; my $cached_subset_data = $self->{'__cache'}->{'is_subset_of'} ||= {}; if (exists $cached_subset_data->{$other_template_id}) { return $cached_subset_data->{$other_template_id}; } unless (ref($other_template) and $self->isa(ref $other_template)) { $cached_subset_data->{$other_template_id} = 0; return 0; } my $my_class = $self->subject_class_name; my $other_class = $other_template->subject_class_name; unless ($my_class eq $other_class or $my_class->isa($other_class)) { $cached_subset_data->{$other_template_id} = undef; return; } my %operators = map { $_ => $self->operator_for($_) } $self->_property_names; my $operators_match = 1; foreach my $prop ( $other_template->_property_names ) { unless (exists $operators{$prop}) { $operators_match = 0; last; } $operators_match = undef if ($operators{$prop} ne $other_template->operator_for($prop)); } $cached_subset_data->{$other_template_id} = $operators_match; return $operators_match; } # This is set lazily currently sub is_unique { my $self = $_[0]; if (defined $self->{is_unique}) { return $self->{is_unique} } # since this requires normalization, we don't set the value at construction time my $normalized_self; if ($self->is_normalized) { $normalized_self = $self; } else { $normalized_self = $self->get_normalized_template_equivalent($self); } my $op = $normalized_self->operator_for('id'); if (defined($op) and ($op eq '' or $op eq '=')) { return $self->{is_unique} = 1; } else { $self->{is_unique} = 0; # if some combination of params can combine to # satisfy at least one unique constraint, # then we have uniqueness in the parameters. if (my @ps = $self->subject_class_name->__meta__->unique_property_sets) { my $property_meta_hash = $self->_property_meta_hash; for my $property_set (@ps) { my $property_set = (ref($property_set) ? $property_set : [$property_set]); my @properties_used_from_constraint = grep { defined($_) } (ref($property_set) ? @$property_meta_hash{@$property_set} : $property_meta_hash->{$property_set}); if (@properties_used_from_constraint == @$property_set) { # filter imprecise operators @properties_used_from_constraint = grep { $_->{operator} !~ /^(not |)like(-.|)$/i and $_->{operator} !~ /^(not |)in/i } @properties_used_from_constraint; if (@properties_used_from_constraint == @$property_set) { $self->{is_unique} = 1; last; } else { ## print "some properties use bad operators: @properties_used_from_constraint\n"; } } else { ## print "too few properties in @properties_used_from_constraint\n"; } } } return $self->{is_unique}; } } # Derivative of the ID. sub rule_template_subclass_name { return "UR::BoolExpr::Template::" . shift->logic_type; } sub get_normalized_template_equivalent { UR::BoolExpr::Template->get($_[0]->{normalized_id}); } sub get_rule_for_values { my $self = shift; my $value_id = UR::BoolExpr::Util->values_to_value_id(@_); my $rule_id = UR::BoolExpr->__meta__->resolve_composite_id_from_ordered_values($self->id,$value_id); my $r = UR::BoolExpr->get($rule_id); # # # FIXME - Don't do this part if the operator is 'in' or 'between' # for (my $i = 0; $i < @_; $i++) { # if (ref($_[$i]) and ! Scalar::Util::blessed($_[$i])) { # $r->{'hard_refs'}->{$i} = $_[$i]; # } # } return $r; } sub get_rule_for_value_id { my $self = shift; my $value_id = shift; my $rule_id = UR::BoolExpr->__meta__->resolve_composite_id_from_ordered_values($self->id,$value_id); return UR::BoolExpr->get($rule_id); } sub extend_params_list_for_values { my $self = shift; #my @prev = @_; my $extenders = $self->normalization_extender_arrayref; if (@$extenders) { my @result; my $subject_class = $self->subject_class_name->__meta__; for my $n (0 .. @$extenders-1) { my $extender = $extenders->[$n]; my ($input_positions_arrayref,$subref,@more_keys) = @$extender; my @more_values = @_[@$input_positions_arrayref]; if ($subref) { ## print "calling $subref on \n\t" . join("\n\t",@more_values) . "\n"; @more_values = $subject_class->$subref(@more_values); ## print "got: \n\t" . join("\n\t",@more_values) . "\n"; } while (@more_keys) { my $k = shift @more_keys; my $v = shift @more_values; push @result, $k => $v; } } return @result; } return (); } sub get_normalized_rule_for_values { my $self = shift; my @unnormalized_values = @_; if ($self->is_normalized) { return $self->get_rule_for_values(@unnormalized_values); } my $normalized_rule_template = $self->get_normalized_template_equivalent; # The normalized rule set may have more values than were actually # passed-in. These 'extenders' will add to the @values array # before re-ordering it. my $extenders = $self->normalization_extender_arrayref; if (@$extenders) { my $subject_class = $self->subject_class_name->__meta__; for my $extender (@$extenders) { my ($input_positions_arrayref,$subref) = @$extender; my @more_values = @unnormalized_values[@$input_positions_arrayref]; if ($subref) { ## print "calling $subref on \n\t" . join("\n\t",@more_values) . "\n"; @more_values = $subject_class->$subref(@more_values); ## print "got: \n\t" . join("\n\t",@more_values) . "\n"; } push @unnormalized_values, @more_values; } } # Normalize the values. Since the normalized template may have added properties, # and a different order we may need to re-order and expand the values list. my $normalized_positions_arrayref = $self->normalized_positions_arrayref; my @normalized_values = @unnormalized_values[@$normalized_positions_arrayref]; my $rule = $normalized_rule_template->get_rule_for_values(@normalized_values); return $rule; } sub _normalize_non_ur_values_hash { my ($self,$unnormalized) = @_; my %normalized; if ($self->subject_class_name ne 'UR::Object::Property') { my $normalized_positions_arrayref = $self->normalized_positions_arrayref; my @reordered_values = @$unnormalized{@$normalized_positions_arrayref}; for (my $n = 0; $n < @reordered_values; $n++) { my $value = $reordered_values[$n]; $normalized{$n} = $value if defined $value; } } return \%normalized; } sub value_position_for_property_name { if (exists $_[0]{_property_meta_hash}{$_[1]}) { return $_[0]{_property_meta_hash}{$_[1]}{value_position}; } else { return undef; } } sub operator_for { if (exists $_[0]{_property_meta_hash}{$_[1]}) { return $_[0]{_property_meta_hash}{$_[1]}{operator} || '='; } else { return undef; } } sub operators_for_properties { my %properties = map { $_ => $_[0]->{'_property_meta_hash'}->{$_}->{'operator'} || '=' } @{ $_[0]->{'_property_names_arrayref'} }; return \%properties; } sub add_filter { my $self = shift; my $property_name = shift; my $op = shift; my $new_key = $property_name; $new_key .= ' ' . $op if defined $op; my ($subject_class_name, $logic_type, $logic_detail) = split("/",$self->id); unless ($logic_type eq 'And') { die "Attempt to add a filter to a rule besides an 'And' rule!"; } my @keys = split(',',$logic_detail); my $new_id = join('/',$subject_class_name,$logic_type,join(',',@keys,$new_key)); return $self->class->get($new_id); } sub remove_filter { my $self = shift; my $filter = shift; my ($subject_class_name, $logic_type, $logic_detail) = split("/",$self->id); my @keys = grep { $_ !~ /^${filter}\b/ } split(',',$logic_detail); my $new_id = join('/',$subject_class_name,$logic_type,join(',',@keys)); #print "$new_id\n"; return $self->class->get($new_id); } sub sub_classify { my ($self,$subclass_name) = @_; my $new_id = $self->id; $new_id =~ s/^.*?\//$subclass_name\//; return $self->class->get($new_id); } # flyweight constructor # NOTE: this caches outside of the regular system since these are stateless objects # NOTE: It's not possible to use this to construct a template with meta-props, like # -hints or -order. To do that, it'll have to also accept constant values as an arg sub get_by_subject_class_name_logic_type_and_logic_detail { my $class = shift; my $subject_class_name = shift; Carp::croak("Expected a subject class name as the first arg of UR::BoolExpr::Template constructor, got " . ( defined($subject_class_name) ? "'$subject_class_name'" : "(undef)" ) ) unless ($subject_class_name); my $logic_type = shift; my $logic_detail = shift; my $constant_value_id = UR::BoolExpr::Util->values_to_value_id(); # intentionally an empty list of values return $class->get(join('/',$subject_class_name,$logic_type,$logic_detail,$constant_value_id)); } # The analogue of resolve in UR::BoolExpr. @params_list is a list if # strings containing properties and operators separated by a space. For ex: "some_param =" sub resolve { my($class,$subject_class_name, @params_list) = @_; return $class->get_by_subject_class_name_logic_type_and_logic_detail($subject_class_name, "And", join(',',@params_list)); } sub get { my $class = shift; my $id = shift; Carp::croak("Non-id params not supported for " . __PACKAGE__ . " yet!") if @_; my $self = $UR::Object::rule_templates->{$id}; return $self if $self; my ($subject_class_name,$logic_type,$logic_detail,$constant_value_id,@extra) = split('/',$id); if (@extra) { # account for a possible slash in the constant value id $constant_value_id = join('/',$constant_value_id,@extra); } # work on the base class or on subclasses my $sub_class_name = ( $class eq __PACKAGE__ ? __PACKAGE__ . "::" . $logic_type : $class ); unless ($logic_type) { Carp::croak("Could not determine logic type from UR::BoolExpr::Template with id $id"); } if ($logic_type eq "And") { # TODO: move into subclass my @keys = split(/,/,$logic_detail || ''); my @constant_values = UR::BoolExpr::Util->value_id_to_values($constant_value_id) if defined $constant_value_id;; return $sub_class_name->_fast_construct( $subject_class_name, \@keys, \@constant_values, $logic_detail, $constant_value_id, ); } else { $self = bless { id => $id, subject_class_name => $subject_class_name, logic_type => $logic_type, logic_detail => $logic_detail, constant_value_id => $constant_value_id, normalized_id => $id, }, $sub_class_name; $UR::Object::rule_templates->{$id} = $self; return $self; } } # Return true if the template has recursion_desc, hints, order or page set sub has_meta_options { my $self = shift; return 1 if @$self{@meta_param_names}; return 0; } # This is the basis for the hash used by the existing UR::Object system for each rule. # this is created upon first request and cached in the object sub legacy_params_hash { my $self = shift; my $legacy_params_hash = $self->{legacy_params_hash}; return $legacy_params_hash if $legacy_params_hash; $legacy_params_hash = {}; my $template_id = $self->id; my $key_op_hash = $self->key_op_hash; my $id_only = $self->is_id_only; my $subject_class_name = $self->subject_class_name; my $logic_type = $self->logic_type; my $logic_detail = $self->logic_detail; my @keys_sorted = $self->_underlying_keys; my $subject_class_meta = $subject_class_name->__meta__; if ( (@keys_sorted and not $logic_detail) or ($logic_detail and not @keys_sorted) ) { Carp::confess(); } if (!$logic_detail) { %$legacy_params_hash = (_unique => 0, _none => 1); } else { # _id_only if ($id_only) { $legacy_params_hash->{_id_only} = 1; } else { $legacy_params_hash->{_id_only} = 0; $legacy_params_hash->{_param_key} = undef; } # _unique if (my $id_op = $key_op_hash->{id}) { if ($id_op->{""} or $id_op->{"="}) { $legacy_params_hash->{_unique} = 1; unless ($self->is_unique) { Carp::carp("The BoolExpr includes a filter on ID, but the is_unique flag is unexpectedly false for $self->{id}"); } } } unless ($legacy_params_hash->{_unique}) { if (defined $legacy_params_hash->{id} and not ref $legacy_params_hash->{id}) { # if we have the id, then we have uniqueness $legacy_params_hash->{_unique} = 1; } else { # default to non-unique $legacy_params_hash->{_unique} = 0; # if some combination of params can combine to # satisfy at least one unique constraint, # then we have uniqueness in the parameters. my @ps = $subject_class_meta->unique_property_sets; for my $property_set (@ps) { my $property_set = (ref($property_set) ? $property_set : [$property_set]); my @properties_used_from_constraint = grep { defined($_) } (ref($property_set) ? @$key_op_hash{@$property_set} : $key_op_hash->{$property_set}); if (@properties_used_from_constraint == @$property_set) { # filter imprecise operators @properties_used_from_constraint = grep { not ( grep { /^(not |)like(-.|)$/i or /^\[\]/} keys %$_ ) } @properties_used_from_constraint; if (@properties_used_from_constraint == @$property_set) { $legacy_params_hash->{_unique} = 1; last; } else { ## print "some properties use bad operators: @properties_used_from_constraint\n"; } } else { ## print "too few properties in @properties_used_from_constraint\n"; } } } # _param_key gets re-set as long as this has a true value $legacy_params_hash->{_param_key} = undef unless $id_only; } } if ($self->is_unique and not $legacy_params_hash->{_unique}) { Carp::carp "is_unique IS set but legacy params hash is NO for $self->{id}"; #$DB::single = 1; $self->is_unique; } if (!$self->is_unique and $legacy_params_hash->{_unique}) { Carp::carp "is_unique NOT set but legacy params hash IS for $self->{id}"; #$DB::single = 1; $self->is_unique; } $self->{legacy_params_hash} = $legacy_params_hash; return $legacy_params_hash; } sub sorter { my $self = shift; # return a standard sorter for expressions using this template # the template might contain a group_by or order_by clause which affects it... die "this method takes no paramters!" if @_; my $class = $self->subject_class_name; my $sort_meta; if ($self->group_by) { my $set_class = $class . "::Set"; $sort_meta = $set_class->__meta__; } else { $sort_meta = $class->__meta__; } my $sorter; if (my $order_by = $self->order_by) { $sorter = $sort_meta->sorter(@$order_by); } else { $sorter = $sort_meta->sorter(); } return $sorter; } 1; BxParser.pm000444023532023421 11773312121654175 16610 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr#################################################################### # # This file was generated using Parse::Yapp version 1.05. # # Don't edit this file, use source file instead. # # ANY CHANGE MADE HERE WILL BE LOST ! # #################################################################### package UR::BoolExpr::BxParser; use vars qw ( @ISA ); use strict; @ISA= qw ( UR::BoolExpr::BxParser::Yapp::Driver ); #Included Parse/Yapp/Driver.pm file---------------------------------------- { # # Module UR::BoolExpr::BxParser::Yapp::Driver # # This module is part of the Parse::Yapp package available on your # nearest CPAN # # Any use of this module in a standalone parser make the included # text under the same copyright as the Parse::Yapp module itself. # # This notice should remain unchanged. # # (c) Copyright 1998-2001 Francois Desarmenien, all rights reserved. # (see the pod text in Parse::Yapp module for use and distribution rights) # package UR::BoolExpr::BxParser::Yapp::Driver; require 5.004; use strict; use vars qw ( $VERSION $COMPATIBLE $FILENAME ); $VERSION = '1.05'; $COMPATIBLE = '0.07'; $FILENAME=__FILE__; use Carp; #Known parameters, all starting with YY (leading YY will be discarded) my(%params)=(YYLEX => 'CODE', 'YYERROR' => 'CODE', YYVERSION => '', YYRULES => 'ARRAY', YYSTATES => 'ARRAY', YYDEBUG => ''); #Mandatory parameters my(@params)=('LEX','RULES','STATES'); sub new { my($class)=shift; my($errst,$nberr,$token,$value,$check,$dotpos); my($self)={ ERROR => \&_Error, ERRST => \$errst, NBERR => \$nberr, TOKEN => \$token, VALUE => \$value, DOTPOS => \$dotpos, STACK => [], DEBUG => 0, CHECK => \$check }; _CheckParams( [], \%params, \@_, $self ); exists($$self{VERSION}) and $$self{VERSION} < $COMPATIBLE and croak "Yapp driver version $VERSION ". "incompatible with version $$self{VERSION}:\n". "Please recompile parser module."; ref($class) and $class=ref($class); bless($self,$class); } sub YYParse { my($self)=shift; my($retval); _CheckParams( \@params, \%params, \@_, $self ); if($$self{DEBUG}) { _DBLoad(); $retval = eval '$self->_DBParse()';#Do not create stab entry on compile $@ and die $@; } else { $retval = $self->_Parse(); } $retval } sub YYData { my($self)=shift; exists($$self{USER}) or $$self{USER}={}; $$self{USER}; } sub YYErrok { my($self)=shift; ${$$self{ERRST}}=0; undef; } sub YYNberr { my($self)=shift; ${$$self{NBERR}}; } sub YYRecovering { my($self)=shift; ${$$self{ERRST}} != 0; } sub YYAbort { my($self)=shift; ${$$self{CHECK}}='ABORT'; undef; } sub YYAccept { my($self)=shift; ${$$self{CHECK}}='ACCEPT'; undef; } sub YYError { my($self)=shift; ${$$self{CHECK}}='ERROR'; undef; } sub YYSemval { my($self)=shift; my($index)= $_[0] - ${$$self{DOTPOS}} - 1; $index < 0 and -$index <= @{$$self{STACK}} and return $$self{STACK}[$index][1]; undef; #Invalid index } sub YYCurtok { my($self)=shift; @_ and ${$$self{TOKEN}}=$_[0]; ${$$self{TOKEN}}; } sub YYCurval { my($self)=shift; @_ and ${$$self{VALUE}}=$_[0]; ${$$self{VALUE}}; } sub YYExpect { my($self)=shift; keys %{$self->{STATES}[$self->{STACK}[-1][0]]{ACTIONS}} } sub YYLexer { my($self)=shift; $$self{LEX}; } ################# # Private stuff # ################# sub _CheckParams { my($mandatory,$checklist,$inarray,$outhash)=@_; my($prm,$value); my($prmlst)={}; while(($prm,$value)=splice(@$inarray,0,2)) { $prm=uc($prm); exists($$checklist{$prm}) or croak("Unknow parameter '$prm'"); ref($value) eq $$checklist{$prm} or croak("Invalid value for parameter '$prm'"); $prm=unpack('@2A*',$prm); $$outhash{$prm}=$value; } for (@$mandatory) { exists($$outhash{$_}) or croak("Missing mandatory parameter '".lc($_)."'"); } } sub _Error { print "Parse error.\n"; } sub _DBLoad { { no strict 'refs'; exists(${__PACKAGE__.'::'}{_DBParse})#Already loaded ? and return; } my($fname)=__FILE__; my(@drv); open(DRV,"<$fname") or die "Report this as a BUG: Cannot open $fname"; while() { /^\s*sub\s+_Parse\s*{\s*$/ .. /^\s*}\s*#\s*_Parse\s*$/ and do { s/^#DBG>//; push(@drv,$_); } } close(DRV); $drv[0]=~s/_P/_DBP/; eval join('',@drv); } #Note that for loading debugging version of the driver, #this file will be parsed from 'sub _Parse' up to '}#_Parse' inclusive. #So, DO NOT remove comment at end of sub !!! sub _Parse { my($self)=shift; my($rules,$states,$lex,$error) = @$self{ 'RULES', 'STATES', 'LEX', 'ERROR' }; my($errstatus,$nberror,$token,$value,$stack,$check,$dotpos) = @$self{ 'ERRST', 'NBERR', 'TOKEN', 'VALUE', 'STACK', 'CHECK', 'DOTPOS' }; #DBG> my($debug)=$$self{DEBUG}; #DBG> my($dbgerror)=0; #DBG> my($ShowCurToken) = sub { #DBG> my($tok)='>'; #DBG> for (split('',$$token)) { #DBG> $tok.= (ord($_) < 32 or ord($_) > 126) #DBG> ? sprintf('<%02X>',ord($_)) #DBG> : $_; #DBG> } #DBG> $tok.='<'; #DBG> }; $$errstatus=0; $$nberror=0; ($$token,$$value)=(undef,undef); @$stack=( [ 0, undef ] ); $$check=''; while(1) { my($actions,$act,$stateno); $stateno=$$stack[-1][0]; $actions=$$states[$stateno]; #DBG> print STDERR ('-' x 40),"\n"; #DBG> $debug & 0x2 #DBG> and print STDERR "In state $stateno:\n"; #DBG> $debug & 0x08 #DBG> and print STDERR "Stack:[". #DBG> join(',',map { $$_[0] } @$stack). #DBG> "]\n"; if (exists($$actions{ACTIONS})) { defined($$token) or do { ($$token,$$value)=&$lex($self); #DBG> $debug & 0x01 #DBG> and print STDERR "Need token. Got ".&$ShowCurToken."\n"; }; $act= exists($$actions{ACTIONS}{$$token}) ? $$actions{ACTIONS}{$$token} : exists($$actions{DEFAULT}) ? $$actions{DEFAULT} : undef; } else { $act=$$actions{DEFAULT}; #DBG> $debug & 0x01 #DBG> and print STDERR "Don't need token.\n"; } defined($act) and do { $act > 0 and do { #shift #DBG> $debug & 0x04 #DBG> and print STDERR "Shift and go to state $act.\n"; $$errstatus and do { --$$errstatus; #DBG> $debug & 0x10 #DBG> and $dbgerror #DBG> and $$errstatus == 0 #DBG> and do { #DBG> print STDERR "**End of Error recovery.\n"; #DBG> $dbgerror=0; #DBG> }; }; push(@$stack,[ $act, $$value ]); $$token ne '' #Don't eat the eof and $$token=$$value=undef; next; }; #reduce my($lhs,$len,$code,@sempar,$semval); ($lhs,$len,$code)=@{$$rules[-$act]}; #DBG> $debug & 0x04 #DBG> and $act #DBG> and print STDERR "Reduce using rule ".-$act." ($lhs,$len): "; $act or $self->YYAccept(); $$dotpos=$len; unpack('A1',$lhs) eq '@' #In line rule and do { $lhs =~ /^\@[0-9]+\-([0-9]+)$/ or die "In line rule name '$lhs' ill formed: ". "report it as a BUG.\n"; $$dotpos = $1; }; @sempar = $$dotpos ? map { $$_[1] } @$stack[ -$$dotpos .. -1 ] : (); $semval = $code ? &$code( $self, @sempar ) : @sempar ? $sempar[0] : undef; splice(@$stack,-$len,$len); $$check eq 'ACCEPT' and do { #DBG> $debug & 0x04 #DBG> and print STDERR "Accept.\n"; return($semval); }; $$check eq 'ABORT' and do { #DBG> $debug & 0x04 #DBG> and print STDERR "Abort.\n"; return(undef); }; #DBG> $debug & 0x04 #DBG> and print STDERR "Back to state $$stack[-1][0], then "; $$check eq 'ERROR' or do { #DBG> $debug & 0x04 #DBG> and print STDERR #DBG> "go to state $$states[$$stack[-1][0]]{GOTOS}{$lhs}.\n"; #DBG> $debug & 0x10 #DBG> and $dbgerror #DBG> and $$errstatus == 0 #DBG> and do { #DBG> print STDERR "**End of Error recovery.\n"; #DBG> $dbgerror=0; #DBG> }; push(@$stack, [ $$states[$$stack[-1][0]]{GOTOS}{$lhs}, $semval ]); $$check=''; next; }; #DBG> $debug & 0x04 #DBG> and print STDERR "Forced Error recovery.\n"; $$check=''; }; #Error $$errstatus or do { $$errstatus = 1; &$error($self); $$errstatus # if 0, then YYErrok has been called or next; # so continue parsing #DBG> $debug & 0x10 #DBG> and do { #DBG> print STDERR "**Entering Error recovery.\n"; #DBG> ++$dbgerror; #DBG> }; ++$$nberror; }; $$errstatus == 3 #The next token is not valid: discard it and do { $$token eq '' # End of input: no hope and do { #DBG> $debug & 0x10 #DBG> and print STDERR "**At eof: aborting.\n"; return(undef); }; #DBG> $debug & 0x10 #DBG> and print STDERR "**Dicard invalid token ".&$ShowCurToken.".\n"; $$token=$$value=undef; }; $$errstatus=3; while( @$stack and ( not exists($$states[$$stack[-1][0]]{ACTIONS}) or not exists($$states[$$stack[-1][0]]{ACTIONS}{error}) or $$states[$$stack[-1][0]]{ACTIONS}{error} <= 0)) { #DBG> $debug & 0x10 #DBG> and print STDERR "**Pop state $$stack[-1][0].\n"; pop(@$stack); } @$stack or do { #DBG> $debug & 0x10 #DBG> and print STDERR "**No state left on stack: aborting.\n"; return(undef); }; #shift the error token #DBG> $debug & 0x10 #DBG> and print STDERR "**Shift \$error token and go to state ". #DBG> $$states[$$stack[-1][0]]{ACTIONS}{error}. #DBG> ".\n"; push(@$stack, [ $$states[$$stack[-1][0]]{ACTIONS}{error}, undef ]); } #never reached croak("Error in driver logic. Please, report it as a BUG"); }#_Parse #DO NOT remove comment 1; } #End of include-------------------------------------------------- sub new { my($class)=shift; ref($class) and $class=ref($class); my($self)=$class->SUPER::new( yyversion => '1.05', yystates => [ {#State 0 ACTIONS => { 'LEFT_PAREN' => 4, 'BETWEEN_WORD' => 11, 'ASC_WORD' => 1, 'DESC_WORD' => 2, 'IDENTIFIER' => 3, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, DEFAULT => -1, GOTOS => { 'boolexpr' => 5, 'expr' => 10, 'keyword_as_value' => 7, 'property' => 15, 'condition' => 14 } }, {#State 1 DEFAULT => -66 }, {#State 2 DEFAULT => -65 }, {#State 3 DEFAULT => -29 }, {#State 4 ACTIONS => { 'ASC_WORD' => 1, 'LEFT_PAREN' => 4, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 16, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 5 ACTIONS => { '' => 17, 'OFFSET' => 18, 'ORDER_BY' => 20, 'GROUP_BY' => 19, 'LIMIT' => 21 } }, {#State 6 DEFAULT => -64 }, {#State 7 DEFAULT => -30 }, {#State 8 DEFAULT => -62 }, {#State 9 DEFAULT => -68 }, {#State 10 ACTIONS => { 'AND' => 22, 'OR' => 23 }, DEFAULT => -2 }, {#State 11 DEFAULT => -63 }, {#State 12 DEFAULT => -67 }, {#State 13 DEFAULT => -61 }, {#State 14 DEFAULT => -7 }, {#State 15 ACTIONS => { 'DOUBLEEQUAL_SIGN' => 24, 'NOT_BANG' => 33, 'NOT_WORD' => 34, 'COLON' => 25, 'LIKE_WORD' => 35, 'OPERATORS' => 26, 'EQUAL_SIGN' => 36, 'FALSE_WORD' => 37, 'BETWEEN_WORD' => 29, 'TILDE' => 38, 'TRUE_WORD' => 40, 'IN_WORD' => 41, 'IS_NOT_NULL' => 44, 'IS_NULL' => 32 }, GOTOS => { 'operator' => 28, 'like_operator' => 30, 'an_operator' => 31, 'in_operator' => 39, 'null_op_word' => 27, 'boolean_op_word' => 43, 'between_operator' => 42, 'negation' => 45 } }, {#State 16 ACTIONS => { 'AND' => 22, 'OR' => 23, 'RIGHT_PAREN' => 46 } }, {#State 17 DEFAULT => 0 }, {#State 18 ACTIONS => { 'INTEGER' => 47 } }, {#State 19 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'group_by_list' => 48, 'property' => 49 } }, {#State 20 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'MINUS' => 50, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'order_by_list' => 53, 'order_by_property' => 51, 'keyword_as_value' => 7, 'property' => 52 } }, {#State 21 ACTIONS => { 'INTEGER' => 54 } }, {#State 22 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'LEFT_PAREN' => 4, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 55, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 23 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'LEFT_PAREN' => 4, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'expr' => 56, 'keyword_as_value' => 7, 'condition' => 14, 'property' => 15 } }, {#State 24 DEFAULT => -45 }, {#State 25 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 59, 'spaces' => 57 } }, {#State 26 DEFAULT => -43 }, {#State 27 DEFAULT => -20 }, {#State 28 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 60, 'spaces' => 57 } }, {#State 29 DEFAULT => -58 }, {#State 30 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'value' => 71, 'keyword_as_value' => 70, 'single_value' => 72, 'like_value' => 66, 'subsequent_value_part' => 65 } }, {#State 31 DEFAULT => -39 }, {#State 32 DEFAULT => -23 }, {#State 33 DEFAULT => -42 }, {#State 34 DEFAULT => -41 }, {#State 35 DEFAULT => -46 }, {#State 36 DEFAULT => -44 }, {#State 37 DEFAULT => -22 }, {#State 38 DEFAULT => -48 }, {#State 39 ACTIONS => { 'LEFT_BRACKET' => 76 }, GOTOS => { 'set' => 77 } }, {#State 40 DEFAULT => -21 }, {#State 41 DEFAULT => -51 }, {#State 42 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 78, 'number' => 64, 'keyword_as_value' => 70, 'single_value' => 79, 'subsequent_value_part' => 65 } }, {#State 43 DEFAULT => -19 }, {#State 44 DEFAULT => -25 }, {#State 45 ACTIONS => { 'DOUBLEEQUAL_SIGN' => 24, 'COLON' => 80, 'LIKE_WORD' => 84, 'OPERATORS' => 26, 'EQUAL_SIGN' => 36, 'BETWEEN_WORD' => 81, 'TILDE' => 85, 'IN_WORD' => 86, 'IS_NULL' => 83 }, GOTOS => { 'an_operator' => 82 } }, {#State 46 DEFAULT => -10 }, {#State 47 DEFAULT => -6 }, {#State 48 DEFAULT => -4 }, {#State 49 ACTIONS => { 'AND' => 87 }, DEFAULT => -37 }, {#State 50 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'property' => 88 } }, {#State 51 ACTIONS => { 'AND' => 89 }, DEFAULT => -35 }, {#State 52 ACTIONS => { 'ASC_WORD' => 90, 'DESC_WORD' => 91 }, DEFAULT => -31 }, {#State 53 DEFAULT => -3 }, {#State 54 DEFAULT => -5 }, {#State 55 ACTIONS => { 'AND' => 22 }, DEFAULT => -8 }, {#State 56 ACTIONS => { 'AND' => 22, 'OR' => 23 }, DEFAULT => -9 }, {#State 57 DEFAULT => -28 }, {#State 58 DEFAULT => -26 }, {#State 59 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 93, 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 92, 'single_value' => 94, 'subsequent_value_part' => 65 } }, {#State 60 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'value' => 95, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 61 DEFAULT => -73 }, {#State 62 DEFAULT => -74 }, {#State 63 ACTIONS => { 'INTEGER' => 97, 'REAL' => 96 } }, {#State 64 DEFAULT => -72 }, {#State 65 DEFAULT => -81 }, {#State 66 DEFAULT => -12 }, {#State 67 DEFAULT => -85 }, {#State 68 DEFAULT => -84 }, {#State 69 DEFAULT => -71 }, {#State 70 DEFAULT => -76 }, {#State 71 DEFAULT => -50 }, {#State 72 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -70, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 100, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 73 DEFAULT => -82 }, {#State 74 DEFAULT => -83 }, {#State 75 DEFAULT => -75 }, {#State 76 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'set_body' => 101, 'number' => 64, 'value' => 102, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 77 DEFAULT => -13 }, {#State 78 DEFAULT => -16 }, {#State 79 ACTIONS => { 'MINUS' => 103 } }, {#State 80 ACTIONS => { 'WHITESPACE' => 58 }, DEFAULT => -27, GOTOS => { 'optional_spaces' => 104, 'spaces' => 57 } }, {#State 81 DEFAULT => -59 }, {#State 82 DEFAULT => -40 }, {#State 83 DEFAULT => -24 }, {#State 84 DEFAULT => -47 }, {#State 85 DEFAULT => -49 }, {#State 86 DEFAULT => -52 }, {#State 87 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'keyword_as_value' => 7, 'group_by_list' => 105, 'property' => 49 } }, {#State 88 DEFAULT => -32 }, {#State 89 ACTIONS => { 'ASC_WORD' => 1, 'IDENTIFIER' => 3, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'MINUS' => 50, 'LIKE_WORD' => 8, 'FALSE_WORD' => 9, 'BETWEEN_WORD' => 11, 'TRUE_WORD' => 12, 'IN_WORD' => 13 }, GOTOS => { 'order_by_list' => 106, 'order_by_property' => 51, 'keyword_as_value' => 7, 'property' => 52 } }, {#State 90 DEFAULT => -34 }, {#State 91 DEFAULT => -33 }, {#State 92 DEFAULT => -14 }, {#State 93 DEFAULT => -17 }, {#State 94 ACTIONS => { 'IN_DIVIDER' => 107, 'MINUS' => 103 } }, {#State 95 DEFAULT => -11 }, {#State 96 DEFAULT => -87 }, {#State 97 DEFAULT => -86 }, {#State 98 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -77, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 108, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 99 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'WHITESPACE' => 58, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'SINGLEQUOTE_STRING' => 75 }, DEFAULT => -80, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'subsequent_values_list' => 109, 'spaces' => 99, 'subsequent_value_part' => 98 } }, {#State 100 DEFAULT => -69 }, {#State 101 ACTIONS => { 'RIGHT_BRACKET' => 110 } }, {#State 102 ACTIONS => { 'SET_SEPARATOR' => 111 }, DEFAULT => -57 }, {#State 103 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'single_value' => 112, 'subsequent_value_part' => 65 } }, {#State 104 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'between_value' => 114, 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 113, 'single_value' => 94, 'subsequent_value_part' => 65 } }, {#State 105 DEFAULT => -38 }, {#State 106 DEFAULT => -36 }, {#State 107 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'number' => 64, 'keyword_as_value' => 70, 'old_syntax_in_value' => 115, 'single_value' => 116, 'subsequent_value_part' => 65 } }, {#State 108 DEFAULT => -78 }, {#State 109 DEFAULT => -79 }, {#State 110 DEFAULT => -55 }, {#State 111 ACTIONS => { 'WORD' => 61, 'DOUBLEQUOTE_STRING' => 62, 'MINUS' => 63, 'BETWEEN_WORD' => 11, 'REAL' => 67, 'INTEGER' => 68, 'ASC_WORD' => 1, 'IDENTIFIER' => 69, 'DESC_WORD' => 2, 'NOT_WORD' => 6, 'LIKE_WORD' => 8, 'AND' => 73, 'FALSE_WORD' => 9, 'TRUE_WORD' => 12, 'IN_WORD' => 13, 'OR' => 74, 'SINGLEQUOTE_STRING' => 75 }, GOTOS => { 'set_body' => 117, 'number' => 64, 'value' => 102, 'keyword_as_value' => 70, 'single_value' => 72, 'subsequent_value_part' => 65 } }, {#State 112 DEFAULT => -60 }, {#State 113 DEFAULT => -15 }, {#State 114 DEFAULT => -18 }, {#State 115 DEFAULT => -53 }, {#State 116 ACTIONS => { 'IN_DIVIDER' => 107 }, DEFAULT => -54 }, {#State 117 DEFAULT => -56 } ], yyrules => [ [#Rule 0 '$start', 2, undef ], [#Rule 1 'boolexpr', 0, sub #line 10 "BxParser.yp" { [] } ], [#Rule 2 'boolexpr', 1, sub #line 11 "BxParser.yp" { UR::BoolExpr::BxParser->_simplify($_[1]) } ], [#Rule 3 'boolexpr', 3, sub #line 12 "BxParser.yp" { [@{$_[1]}, '-order', $_[3]] } ], [#Rule 4 'boolexpr', 3, sub #line 13 "BxParser.yp" { [@{$_[1]}, '-group', $_[3]] } ], [#Rule 5 'boolexpr', 3, sub #line 14 "BxParser.yp" { [@{$_[1]}, '-limit', $_[3]] } ], [#Rule 6 'boolexpr', 3, sub #line 15 "BxParser.yp" { [@{$_[1]}, '-offset', $_[3]] } ], [#Rule 7 'expr', 1, sub #line 18 "BxParser.yp" { $_[1] } ], [#Rule 8 'expr', 3, sub #line 19 "BxParser.yp" { UR::BoolExpr::BxParser->_and($_[1], $_[3]) } ], [#Rule 9 'expr', 3, sub #line 20 "BxParser.yp" { UR::BoolExpr::BxParser->_or($_[1], $_[3]) } ], [#Rule 10 'expr', 3, sub #line 21 "BxParser.yp" { $_[2] } ], [#Rule 11 'condition', 4, sub #line 24 "BxParser.yp" { [ "$_[1] $_[2]" => $_[4] ] } ], [#Rule 12 'condition', 3, sub #line 25 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 13 'condition', 3, sub #line 26 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 14 'condition', 4, sub #line 27 "BxParser.yp" { [ "$_[1] in" => $_[4] ] } ], [#Rule 15 'condition', 5, sub #line 28 "BxParser.yp" { [ "$_[1] $_[2] in" => $_[5] ] } ], [#Rule 16 'condition', 3, sub #line 29 "BxParser.yp" { [ "$_[1] $_[2]" => $_[3] ] } ], [#Rule 17 'condition', 4, sub #line 30 "BxParser.yp" { [ "$_[1] between" => $_[4] ] } ], [#Rule 18 'condition', 5, sub #line 31 "BxParser.yp" { [ "$_[1] $_[2] between" => $_[5] ] } ], [#Rule 19 'condition', 2, sub #line 32 "BxParser.yp" { [ "$_[1] $_[2]" => 1 ] } ], [#Rule 20 'condition', 2, sub #line 33 "BxParser.yp" { [ "$_[1] $_[2]" => undef ] } ], [#Rule 21 'boolean_op_word', 1, sub #line 36 "BxParser.yp" { $_[1] } ], [#Rule 22 'boolean_op_word', 1, sub #line 37 "BxParser.yp" { $_[1] } ], [#Rule 23 'null_op_word', 1, sub #line 40 "BxParser.yp" { '=' } ], [#Rule 24 'null_op_word', 2, sub #line 41 "BxParser.yp" { "!=" } ], [#Rule 25 'null_op_word', 1, sub #line 42 "BxParser.yp" { '!=' } ], [#Rule 26 'spaces', 1, sub #line 45 "BxParser.yp" { $_[1] } ], [#Rule 27 'optional_spaces', 0, sub #line 48 "BxParser.yp" { undef } ], [#Rule 28 'optional_spaces', 1, sub #line 49 "BxParser.yp" { undef } ], [#Rule 29 'property', 1, sub #line 52 "BxParser.yp" { $_[1] } ], [#Rule 30 'property', 1, sub #line 53 "BxParser.yp" { $_[1] } ], [#Rule 31 'order_by_property', 1, sub #line 56 "BxParser.yp" { $_[1 ] } ], [#Rule 32 'order_by_property', 2, sub #line 57 "BxParser.yp" { '-'.$_[2] } ], [#Rule 33 'order_by_property', 2, sub #line 58 "BxParser.yp" { '-'.$_[1] } ], [#Rule 34 'order_by_property', 2, sub #line 59 "BxParser.yp" { $_[1] } ], [#Rule 35 'order_by_list', 1, sub #line 62 "BxParser.yp" { [ $_[1]] } ], [#Rule 36 'order_by_list', 3, sub #line 63 "BxParser.yp" { [$_[1], @{$_[3]}] } ], [#Rule 37 'group_by_list', 1, sub #line 66 "BxParser.yp" { [ $_[1] ] } ], [#Rule 38 'group_by_list', 3, sub #line 67 "BxParser.yp" { [$_[1], @{$_[3]}] } ], [#Rule 39 'operator', 1, sub #line 70 "BxParser.yp" { $_[1] } ], [#Rule 40 'operator', 2, sub #line 71 "BxParser.yp" { "$_[1] $_[2]" } ], [#Rule 41 'negation', 1, sub #line 74 "BxParser.yp" { 'not' } ], [#Rule 42 'negation', 1, sub #line 75 "BxParser.yp" { 'not' } ], [#Rule 43 'an_operator', 1, sub #line 78 "BxParser.yp" { $_[1] } ], [#Rule 44 'an_operator', 1, sub #line 79 "BxParser.yp" { '=' } ], [#Rule 45 'an_operator', 1, sub #line 80 "BxParser.yp" { '=' } ], [#Rule 46 'like_operator', 1, sub #line 83 "BxParser.yp" { 'like' } ], [#Rule 47 'like_operator', 2, sub #line 84 "BxParser.yp" { "$_[1] like" } ], [#Rule 48 'like_operator', 1, sub #line 85 "BxParser.yp" { 'like' } ], [#Rule 49 'like_operator', 2, sub #line 86 "BxParser.yp" { "$_[1] like" } ], [#Rule 50 'like_value', 1, sub #line 89 "BxParser.yp" { $_[1] =~ m/\%/ ? $_[1] : '%' . $_[1] . '%' } ], [#Rule 51 'in_operator', 1, sub #line 92 "BxParser.yp" { 'in' } ], [#Rule 52 'in_operator', 2, sub #line 93 "BxParser.yp" { "$_[1] in" } ], [#Rule 53 'old_syntax_in_value', 3, sub #line 96 "BxParser.yp" { [ $_[1], @{$_[3]} ] } ], [#Rule 54 'old_syntax_in_value', 3, sub #line 97 "BxParser.yp" { [ $_[1], $_[3] ] } ], [#Rule 55 'set', 3, sub #line 100 "BxParser.yp" { $_[2] } ], [#Rule 56 'set_body', 3, sub #line 103 "BxParser.yp" { [ $_[1], @{$_[3]} ] } ], [#Rule 57 'set_body', 1, sub #line 104 "BxParser.yp" { [ $_[1] ] } ], [#Rule 58 'between_operator', 1, sub #line 107 "BxParser.yp" { 'between' } ], [#Rule 59 'between_operator', 2, sub #line 108 "BxParser.yp" { "$_[1] between" } ], [#Rule 60 'between_value', 3, sub #line 111 "BxParser.yp" { [ $_[1], $_[3] ] } ], [#Rule 61 'keyword_as_value', 1, sub #line 114 "BxParser.yp" { $_[1] } ], [#Rule 62 'keyword_as_value', 1, sub #line 115 "BxParser.yp" { $_[1] } ], [#Rule 63 'keyword_as_value', 1, sub #line 116 "BxParser.yp" { $_[1] } ], [#Rule 64 'keyword_as_value', 1, sub #line 117 "BxParser.yp" { $_[1] } ], [#Rule 65 'keyword_as_value', 1, sub #line 118 "BxParser.yp" { $_[1] } ], [#Rule 66 'keyword_as_value', 1, sub #line 119 "BxParser.yp" { $_[1] } ], [#Rule 67 'keyword_as_value', 1, sub #line 120 "BxParser.yp" { $_[1] } ], [#Rule 68 'keyword_as_value', 1, sub #line 121 "BxParser.yp" { $_[1] } ], [#Rule 69 'value', 2, sub #line 124 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 70 'value', 1, sub #line 125 "BxParser.yp" { $_[1] } ], [#Rule 71 'subsequent_value_part', 1, sub #line 128 "BxParser.yp" { $_[1] } ], [#Rule 72 'subsequent_value_part', 1, sub #line 129 "BxParser.yp" { $_[1] } ], [#Rule 73 'subsequent_value_part', 1, sub #line 130 "BxParser.yp" { $_[1] } ], [#Rule 74 'subsequent_value_part', 1, sub #line 131 "BxParser.yp" { ($_[1] =~ m/^"(.*?)"$/)[0]; } ], [#Rule 75 'subsequent_value_part', 1, sub #line 132 "BxParser.yp" { ($_[1] =~ m/^'(.*?)'$/)[0]; } ], [#Rule 76 'subsequent_value_part', 1, sub #line 133 "BxParser.yp" { $_[1] } ], [#Rule 77 'subsequent_values_list', 1, sub #line 136 "BxParser.yp" { $_[1] } ], [#Rule 78 'subsequent_values_list', 2, sub #line 137 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 79 'subsequent_values_list', 2, sub #line 138 "BxParser.yp" { $_[1].$_[2] } ], [#Rule 80 'subsequent_values_list', 1, sub #line 139 "BxParser.yp" { '' } ], [#Rule 81 'single_value', 1, sub #line 142 "BxParser.yp" { $_[1] } ], [#Rule 82 'single_value', 1, sub #line 143 "BxParser.yp" { $_[1] } ], [#Rule 83 'single_value', 1, sub #line 144 "BxParser.yp" { $_[1] } ], [#Rule 84 'number', 1, sub #line 148 "BxParser.yp" { $_[1] + 0 } ], [#Rule 85 'number', 1, sub #line 149 "BxParser.yp" { $_[1] + 0 } ], [#Rule 86 'number', 2, sub #line 150 "BxParser.yp" { 0 - $_[2] } ], [#Rule 87 'number', 2, sub #line 151 "BxParser.yp" { 0 - $_[2] } ] ], @_); bless($self,$class); } #line 154 "BxParser.yp" package UR::BoolExpr::BxParser; use strict; use warnings; sub _error { my @expect = $_[0]->YYExpect; my $tok = $_[0]->YYData->{INPUT}; my $match = $_[0]->YYData->{MATCH}; my $string = $_[0]->YYData->{STRING}; my $err = qq(Can't parse expression "$string"\n Syntax error near token $tok '$match'); my $rem = $_[0]->YYData->{REMAINING}; $err .= ", remaining text: '$rem'" if $rem; $err .= "\nExpected one of: " . join(", ", @expect) . "\n"; Carp::croak($err); } my %token_states = ( 'DEFAULT' => [ WHITESPACE => qr{\s+}, AND => [ qr{and}i, 'DEFAULT'], OR => [ qr{or}i, 'DEFAULT' ], BETWEEN_WORD => qr{between}, LIKE_WORD => qr{like}, IN_WORD => qr{in}, NOT_WORD => qr{not}, DESC_WORD => qr{desc}, ASC_WORD => qr{asc}, TRUE_WORD => qr{true}, FALSE_WORD => qr{false}, LIMIT => qr{limit}, OFFSET => qr{offset}, IDENTIFIER => qr{[a-zA-Z_][a-zA-Z0-9_.]*}, MINUS => qr{-}, INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, LEFT_PAREN => [ qr{\(}, 'DEFAULT' ], RIGHT_PAREN => [ qr{\)}, 'DEFAULT' ], LEFT_BRACKET => [ qr{\[}, 'set_contents'], RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], NOT_BANG => qr{!}, EQUAL_SIGN => [ qr{=}, 'dont_gobble_spaces' ], DOUBLEEQUAL_SIGN => [ qr{=>}, 'dont_gobble_spaces' ], OPERATORS => [ qr{<=|>=|<|>}, 'dont_gobble_spaces' ], AND => [ qr{,}, 'DEFAULT' ], COLON => [ qr{:}, 'after_colon_value' ], TILDE => qr{~}, ORDER_BY => qr{order by}, GROUP_BY => qr{group by}, IS_NULL => qr{is null|is undef}, IS_NOT_NULL => qr{is not null|is not undef}, ], 'set_contents' => [ SET_SEPARATOR => qr{,}, # Depending on state, can be either AND or SET_SEPARATOR WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards RIGHT_BRACKET => [qr{\]}, 'DEFAULT' ], ], 'after_colon_value' => [ INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, IN_DIVIDER => qr{\/}, #WORD => qr{\w+}, # Override WORD in DEFAULT to disallow / WORD => qr{[%\+\.\w\:][\+\.\:%\w]*}, # Override WORD in DEFAULT to disallow / DOUBLEQUOTE_STRING => qr{"(?:\\.|[^"])*"}, SINGLEQUOTE_STRING => qr{'(?:\\.|[^'])*'}, WHITESPACE => [qr{\s+}, 'DEFAULT'], ], 'dont_gobble_spaces' => [ AND => [ qr{and}, 'DEFAULT'], OR => [ qr{or}, 'DEFAULT' ], LIMIT => [qr{limit}, 'DEFAULT'], OFFSET => [qr{offset}, 'DEFAULT'], INTEGER => qr{\d+}, REAL => qr{\d*\.\d+|\d+\.\d*}, WORD => qr{[%\+\.\/\w][\+\-\.\:%\w\/]*}, # also allow / for pathnames, - for hyphenated names, % for like wildcards ORDER_BY => [qr{order by}, 'DEFAULT'], GROUP_BY => [qr{group by}, 'DEFAULT'], ], ); sub parse { my $string = shift; my %params = @_; my $debug = $params{'tokdebug'}; my $yydebug = $params{'yydebug'} || 0; print "\nStarting parse for string $string\n" if $debug; my $parser = UR::BoolExpr::BxParser->new(); $parser->YYData->{STRING} = $string; my $parser_state = 'DEFAULT'; my $get_next_token = sub { if (length($string) == 0) { print "String is empty, we're done!\n" if $debug; return (undef, ''); } GET_NEXT_TOKEN: foreach (1) { my $longest = 0; my $longest_token = ''; my $longest_match = ''; for my $token_list ( $parser_state, 'DEFAULT' ) { print "\nTrying tokens for state $token_list...\n" if $debug; my $tokens = $token_states{$token_list}; for(my $i = 0; $i < @$tokens; $i += 2) { my($tok, $re) = @$tokens[$i, $i+1]; print "Trying token $tok... " if $debug; my($regex,$next_parser_state); if (ref($re) eq 'ARRAY') { ($regex,$next_parser_state) = @$re; } else { $regex = $re; } if ($string =~ m/^($regex)/) { print "Matched >>$1<<" if $debug; my $match_len = length($1); if ($match_len > $longest) { print "\n ** It's now the longest" if $debug; $longest = $match_len; $longest_token = $tok; $longest_match = $1; if ($next_parser_state) { $parser_state = $next_parser_state; } } } print "\n" if $debug; } $string = substr($string, $longest); print "Consuming up to char pos $longest chars, string is now >>$string<<\n" if $debug; if ($longest_token eq 'WHITESPACE' and $parser_state ne 'dont_gobble_spaces') { print "Redoing token extraction after whitespace\n" if $debug; redo GET_NEXT_TOKEN; } $parser->YYData->{REMAINING} = $string; if ($longest) { print "Returning token $longest_token, match $longest_match\n next state is named $parser_state\n" if $debug; $parser->YYData->{INPUT} = $longest_token; $parser->YYData->{MATCH} = $longest_match; return ($longest_token, $longest_match); } last if $token_list eq 'DEFAULT'; # avoid going over it twice if $parser_state is DEFAULT } } print "Didn't match anything, done!\n" if $debug; return (undef, ''); # Didn't match anything }; return ( $parser->YYParse( yylex => $get_next_token, yyerror => \&_error, yydebug => $yydebug), \$string, ); } # Used by the top-level expr production to turn an or-type parse tree with # only a single AND condition into a simple AND-type tree (1-level arrayref). # Or to add the '-or' to the front of a real OR-type tree so it can be passed # directly to UR::BoolExpr::resolve() sub _simplify { my($class, $expr) = @_; if (ref($expr->[0])) { if (@$expr == 1) { # An or-type parse tree, but with only one AND subrule - use as a simple and-type rule $expr = $expr->[0]; } else { $expr = ['-or', $expr]; # an or-type parse tree with multiple subrules } } return $expr; } # Handles the case for "expr AND expr" where one or both exprs can be an # OR-type expr. In that case, it distributes the AND exprs among all the # OR conditions. For example: # (a=1 or b=2) and (c=3 or d=4) # is the same as # (a=1 and c=3) or (a=1 and d=4) or (b=2 and c=3) or (b=2 and d=4) # This is necessary because the BoolExpr resolver can only handle 1-level deep # AND-type rules, or a 1-level deep OR-type rule composed of any number of # 1-level deep AND-type rules sub _and { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); my @and; foreach my $left_subexpr ( @$left ) { foreach my $right_subexpr (@$right) { push @and, [@$left_subexpr, @$right_subexpr]; } } \@and; } sub _or { my($class,$left, $right) = @_; # force them to be [[ "property operator" => value]] instead of just [ "property operator" => value ] $left = [ $left ] unless (ref($left->[0])); $right = [ $right ] unless (ref($right->[0])); [ @$left, @$right ]; } 1; 1; Template000755023532023421 012121654174 16065 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExprOr.pm000444023532023421 1313212121654172 17156 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Templatepackage UR::BoolExpr::Template::Or; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION;; require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::Composite'], ); sub _flatten_bx { my ($class, $bx) = @_; my @old = $bx->underlying_rules; my @new; for my $old (@old) { my $new = $old->flatten; push @new, [ $new->_params_list ]; } my $flattened_bx = $class->_compose($bx->subject_class_name,\@new); return $flattened_bx; } sub _reframe_bx { my ($class, $bx, $in_terms_of) = @_; my @old = $bx->underlying_rules; my @new; for my $old (@old) { my $new = $old->reframe($in_terms_of); push @new, [ $new->_params_list ]; } my @meta = $bx->subject_class_name->__meta__->property_meta_for_name($in_terms_of); my @joins = $meta[-1]->_resolve_join_chain($in_terms_of); my $reframed_bx = $class->_compose($joins[-1]{foreign_class},\@new); return $reframed_bx; } sub _compose { my $self = shift; my $subject_class = shift; my $sub_queries = shift; my $meta_params = shift; my @underlying_rules; my @expressions; my @values; while (@$sub_queries) { my $underlying_query; if (ref($sub_queries->[0]) eq 'ARRAY') { $underlying_query = UR::BoolExpr->resolve($subject_class, @{$sub_queries->[0]}, @$meta_params); shift @$sub_queries; } elsif (ref($sub_queries->[0]) eq 'UR::BoolExpr::And') { $underlying_query = shift @$sub_queries; } else { $underlying_query = UR::BoolExpr->resolve($subject_class, @$sub_queries[0,1], @$meta_params); shift @$sub_queries; shift @$sub_queries; } if ($underlying_query->{'_constant_values'}) { Carp::confess("cannot use -* expressions in subordinate clauses of a logical "); } unless ($underlying_query->template->isa("UR::BoolExpr::Template::And")) { Carp::confess("$underlying_query is not an AND template"); } push @underlying_rules, $underlying_query; push @expressions, $underlying_query->template->logic_detail; push @values, $underlying_query->values; } my $bxt = UR::BoolExpr::Template::Or->get_by_subject_class_name_logic_type_and_logic_detail($subject_class,'Or',join('|',@expressions)); my $bx = $bxt->get_rule_for_values(@values); # This (and accompanying "caching" in UR::BoolExpr::underlying_rules()) # is a giant hack to allow composite rules to have -order and -group # The real fix is to coax the above combination of # get_by_subject_class_name_logic_type_and_logic_detail() and get_rule_for_values() to # properly encode these constant/template values into the rule and template IDs, # and subsequently reconsitiute them when you call $template->order_by $bx->{'_underlying_rules'} = \@underlying_rules; for (my $i = 0; $i < @$meta_params; $i += 2) { my $method = $meta_params->[$i]; substr($method, 0, 1, ''); # remove the - if ($method eq 'recurse') { $bx->template->recursion_desc($meta_params->[$i + 1]); } elsif ($method eq 'order') { $bx->template->order_by($meta_params->[$i + 1]); } else { $bx->template->$method($meta_params->[$i + 1]); } } return $bx; } sub _underlying_keys { my $self = shift; my $logic_detail = $self->logic_detail; return unless $logic_detail; my @underlying_keys = split('\|',$logic_detail); return @underlying_keys; } # sub get_underlying_rules_for_values sub get_underlying_rule_templates { my $self = shift; my @underlying_keys = $self->_underlying_keys(); my $subject_class_name = $self->subject_class_name; return map { UR::BoolExpr::Template::And ->_get_for_subject_class_name_and_logic_detail( $subject_class_name, $_ ); } @underlying_keys; } sub specifies_value_for { my ($self, $property_name) = @_; Carp::confess() if not defined $property_name; my @underlying_templates = $self->get_underlying_rule_templates(); my @all_specified; for my $template (@underlying_templates) { my @specified = $template->specifies_value_for($property_name); if (@specified) { push @all_specified, @specified; } else { return; } } return @all_specified; } sub evaluate_subject_and_values { my $self = shift; my $subject = shift; return unless (ref($subject) && $subject->isa($self->subject_class_name)); my @underlying = $self->get_underlying_rule_templates; while (my $underlying = shift (@underlying)) { my $n = $underlying->_variable_value_count; my @next_values = splice(@_,0,$n); if ($underlying->evaluate_subject_and_values($subject,@_)) { return 1; } } return; } sub params_list_for_values { my $self = shift; my @values_sorted = @_; my @list; my @t = $self->get_underlying_rule_templates; for my $t (@t) { my $c = $t->_variable_value_count; my @l = $t->params_list_for_values(splice(@values_sorted,0,$c)); push @list, \@l; } return -or => \@list; } sub get_normalized_rule_for_values { my $self = shift; return $self->get_rule_for_values(@_); } 1; =pod =head1 NAME UR::BoolExpr::Or - a rule which is true if ANY of the underlying conditions are true =head1 SEE ALSO UR::BoolExpr;(3) =cut And.pm000444023532023421 10540312121654173 17324 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Templatepackage UR::BoolExpr::Template::And; use warnings; use strict; require UR; our $VERSION = "0.41"; # UR $VERSION;; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::Composite'], ); sub _flatten_bx { my ($class, $bx) = @_; my $template = $bx->template; my ($flattened_template, @extra_values) = $template->_flatten(@_); my $flattened_bx; if (not @extra_values) { # optimized my $flattened_bx_id = $flattened_template->id . $UR::BoolExpr::Util::id_sep . $bx->value_id; $flattened_bx = UR::BoolExpr->get($flattened_bx_id); $flattened_bx->{'values'} = $bx->{'values'} unless $flattened_bx->{'values'}; } else { $flattened_bx = $flattened_template->get_rule_for_values($bx->values, @extra_values); } return $flattened_bx; } sub _reframe_bx { my ($class, $bx, $in_terms_of_property_name) = @_; my $template = $bx->template; my ($reframed_template, @extra_values) = $template->_reframe($in_terms_of_property_name); my $reframed_bx; if (@extra_values == 0) { my $reframed_bx_id = $reframed_template->id . $UR::BoolExpr::Util::id_sep . $bx->value_id; $reframed_bx = UR::BoolExpr->get($reframed_bx_id); $reframed_bx->{'values'} = $bx->{'values'} unless $reframed_bx->{'values'}; } else { my @values = ($bx->values, @extra_values); $reframed_bx = $reframed_template->get_rule_for_values(@values); } return $reframed_bx; } sub _flatten { my $self = $_[0]; if ($self->{flatten}) { return @{ $self->{flatten} } } my @old_keys = @{ $self->_keys }; my $old_property_meta_hash = $self->_property_meta_hash; my $class_meta = $self->subject_class_name->__meta__; my @new_keys; my @extra_keys; my @extra_values; my $old_constant_values; my @new_constant_values; my $found_unflattened_params = 0; while (my $key = shift @old_keys) { my $name = $key; $name =~ s/ .*//; if (substr($name,0,1) ne '-') { my $mdata = $old_property_meta_hash->{$name}; my ($value_position, $operator) = @$mdata{'value_position','operator'}; my ($flat, $add_keys, $add_values) = $class_meta->_flatten_property_name($name); $found_unflattened_params = 1 if $flat ne $name or @$add_keys or @$add_values; $flat .= ' ' . $operator if $operator and $operator ne '='; push @new_keys, $flat; push @extra_keys, @$add_keys; push @extra_values, @$add_values; } else { push @new_keys, $key; $old_constant_values ||= [ @{ $self->_constant_values } ]; my $old_value = shift @$old_constant_values; my $new_value = []; for my $part (@$old_value) { my ($flat, $add_keys, $add_values) = $class_meta->_flatten_property_name($part); $found_unflattened_params = 1 if $flat ne $name or @$add_keys or @$add_values; push @$new_value, $flat; push @extra_keys, @$add_keys; push @extra_values, @$add_values; } push @new_constant_values, $new_value; } } my $constant_values; if ($old_constant_values) { # some -* keys were found above, and we flattened the value internals $constant_values = \@new_constant_values; } else { # no -* keys, just re-use the empty arrayref $constant_values = $self->_constant_values; } if ($found_unflattened_params or @extra_keys) { if (@extra_keys) { # there may be duplication between these and the primary joins # or each other my %keys_seen = map { $_ => 1 } @new_keys; my @nodup_extra_keys; my @nodup_extra_values; while (my $extra_key = shift @extra_keys) { my $extra_value = shift @extra_values; unless ($keys_seen{$extra_key}) { push @nodup_extra_keys, $extra_key; push @nodup_extra_values, $extra_value; $keys_seen{$extra_key} = 1; } } push @new_keys, @nodup_extra_keys; @extra_values = @nodup_extra_values } my $flat = UR::BoolExpr::Template::And->_fast_construct( $self->subject_class_name, \@new_keys, $constant_values, ); $self->{flatten} = [$flat,@extra_values]; return ($flat, @extra_values); } else { # everything was already flat, just remember this so you DRY $self->{flatten} = [$self]; Scalar::Util::weaken($self->{flatten}[0]); return $self } } sub _reframe { my $self = shift; my $in_terms_of_property_name = shift; # determine the from_class, to_class, and path_back my $from_class = $self->subject_class_name; my $cmeta = $self->subject_class_name->__meta__; my @pmeta = $cmeta->property_meta_for_name($in_terms_of_property_name); unless (@pmeta) { Carp::confess("Failed to find property $in_terms_of_property_name on $from_class. Cannot reframe $self!"); } my @reframe_path_forward = map { $_->_resolve_join_chain($in_terms_of_property_name) } @pmeta; my $to_class = $reframe_path_forward[-1]{foreign_class}; # translate all of the old properties to use the path back to the original class my ($flat,@extra_values) = $self->_flatten; my @old_keys = @{ $flat->_keys }; my $old_property_meta_hash = $flat->_property_meta_hash; my %sub_group_label_used; my $reframer = sub { my $old_name = $_[0]; # uses: @reframe_path_forward from above in this closure # get back to the original object my @reframe_path_back = reverse @reframe_path_forward; # then forward to the property related to it my @filter_path_forward = split('\.',$old_name); # if the end of the path back matches the beginning of the path # to the property in the expression unneeded steps (beyond 1) my $new_key; while (1) { unless (@reframe_path_back) { last; } unless (@filter_path_forward) { last; } my $last_name_back = $reframe_path_back[-1]{source_name_for_foreign}; my $first_name_forward = $filter_path_forward[0]; my $turnaround_match = 0; if ($last_name_back eq $first_name_forward) { # complete overlap $turnaround_match = 1; # safe } else { # see if stripping off any labels makes them match my $last_name_back_base = $last_name_back; $last_name_back_base =~ s/-.*//; my $first_name_forward_base = $first_name_forward; $first_name_forward_base =~ s/-.*//; if ($last_name_back_base eq $first_name_forward_base) { # removing the grouping label causes a match # possible overlap for my $pair ( [$first_name_forward_base, $last_name_back], [$last_name_back_base, $first_name_forward], ) { my ($partial, $full) = @$pair; if (index($full, $partial) == 0) { #print "$partial is part of $full\n"; if (my $prev_full = $sub_group_label_used{$partial}) { # we've tracked back through this $partially specified relationship once # see if we did it the same way if ($prev_full eq $full) { $turnaround_match = 1; } else { #print "previously used $prev_full for $partial: cannot use $full\n"; next; } } else { # this relationship has not been seen #print "using $full for $partial\n"; $sub_group_label_used{$partial} = $full; $turnaround_match = 1; } } } } } if ($turnaround_match == 0) { # found a difference: no shortcut # we have to trek all the way back to the original subject before # moving forward to this property last; } else { # the last step back matches the first step to the property if (@reframe_path_back == 1 and @filter_path_forward == 1) { # just keep one of the identical pair shift @filter_path_forward; } else { # remove both (if one is empty this is no problem) pop @reframe_path_back; shift @filter_path_forward; } } } $new_key = join('.', map { $_->{foreign_name_for_source} } @reframe_path_back); $new_key = join('.', ($new_key ? $new_key : ()), @filter_path_forward); return $new_key; }; # this is only set below if we find any -* keys my $old_constant_values; my @new_keys; my @new_constant_values; while (@old_keys) { my $old_key = shift @old_keys; if (substr($old_key,0,1) ne '-') { # a regular property my $old_name = $old_key; $old_name =~ s/ .*//; my $mdata = $old_property_meta_hash->{$old_name}; my ($value_position, $operator) = @$mdata{'value_position','operator'}; my $new_key = $reframer->($old_name); $new_key .= ' ' . $operator if $operator and $operator ne '='; push @new_keys, $new_key; } else { # this key is not a property, it's a special key like -order_by or -group_by unless ($old_key eq '-order_by' or $old_key eq '-group_by' or $old_key eq '-hints' or $old_key eq '-recurse' ) { Carp::confess("no support yet for $old_key in bx reframe()!"); } push @new_keys, $old_key; unless ($old_constant_values) { $old_constant_values = [ @{ $flat->_constant_values } ]; } my $old_value = shift @$old_constant_values; my $new_value = []; for my $part (@$old_value) { my $reframed_part = $reframer->($part); push @$new_value, $reframed_part; } push @new_constant_values, $new_value; } } my $constant_values; if (@new_constant_values) { $constant_values = \@new_constant_values; } else { $constant_values = $flat->_constant_values; # re-use empty immutable arrayref } my $reframed = UR::BoolExpr::Template::And->_fast_construct( $to_class, \@new_keys, $constant_values, ); return $reframed, @extra_values; } sub _template_for_grouped_subsets { my $self = shift; my $group_by = $self->group_by; die "rule template $self->{id} has no -group_by!?!?" unless $group_by; my @base_property_names = $self->_property_names; for (my $i = 0; $i < @base_property_names; $i++) { my $operator = $self->operator_for($base_property_names[$i]); if ($operator ne '=') { $base_property_names[$i] .= " $operator"; } } my $template = UR::BoolExpr::Template->get_by_subject_class_name_logic_type_and_logic_detail( $self->subject_class_name, 'And', join(",", @base_property_names, @$group_by), ); return $template; } sub _variable_value_count { my $self = shift; my $k = $self->_underlying_keys; my $v = $self->_constant_values; if ($v) { $v = scalar(@$v); } else { $v = 0; } return $k-$v; } sub _underlying_keys { my $self = shift; my $logic_detail = $self->logic_detail; return unless $logic_detail; my @underlying_keys = split(",",$logic_detail); return @underlying_keys; } sub get_underlying_rule_templates { my $self = shift; my @underlying_keys = grep { substr($_,0,1) eq '-' ? () : ($_) } $self->_underlying_keys(); my $subject_class_name = $self->subject_class_name; return map { UR::BoolExpr::Template::PropertyComparison ->_get_for_subject_class_name_and_logic_detail( $subject_class_name, $_ ); } @underlying_keys; } sub specifies_value_for { my ($self, $property_name) = @_; Carp::confess('Missing required parameter property_name for specifies_value_for()') if not defined $property_name; my @underlying_templates = $self->get_underlying_rule_templates(); foreach ( @underlying_templates ) { return 1 if $property_name eq $_->property_name; } return; } sub _filter_breakdown { my $self = $_[0]; my $filter_breakdown = $self->{_filter_breakdown} ||= do { my @underlying = $self->get_underlying_rule_templates; my @primary; my %sub_group_filters; my %sub_group_sub_filters; for (my $n = 0; $n < @underlying; $n++) { my $underlying = $underlying[$n]; my $sub_group = $underlying->sub_group; if ($sub_group) { if (substr($sub_group,-1) ne '?') { # control restruct the subject based on the sub-group properties my $list = $sub_group_filters{$sub_group} ||= []; push @$list, $underlying, $n; } else { # control what is IN a sub-group (effectively define it with these) chop($sub_group); my $list = $sub_group_sub_filters{$sub_group} ||= []; push @$list, $underlying, $n; } } else { push @primary, $underlying, $n; } } { primary => \@primary, sub_group_filters => \%sub_group_filters, sub_group_sub_filters => \%sub_group_sub_filters, }; }; return $filter_breakdown; } sub evaluate_subject_and_values { my $self = shift; my $subject = shift; return unless (ref($subject) && $subject->isa($self->subject_class_name)); my $filter_breakdown = $self->_filter_breakdown; my ($primary,$sub_group_filters,$sub_group_sub_filters) = @$filter_breakdown{"primary","sub_group_filters","sub_group_sub_filters"}; # flattening expresions now requires that we re-group them :( # these effectively are subqueries where they occur # check the ungrouped comparisons first since they are simpler for (my $n = 0; $n < @$primary; $n+=2) { my $underlying = $primary->[$n]; my $pos = $primary->[$n+1]; my $value = $_[$pos]; unless ($underlying->evaluate_subject_and_values($subject, $value)) { return; } } # only check the complicated rules if none of the above failed if (%$sub_group_filters) { #$DB::single = 1; for my $sub_group (keys %$sub_group_filters) { my $filters = $sub_group_filters->{$sub_group}; my $sub_filters = $sub_group_sub_filters->{$sub_group}; print "FILTERING $sub_group: " . Data::Dumper::Dumper($filters, $sub_filters); } } return 1; } sub params_list_for_values { # This is the reverse of the bulk of resolve. # It returns the params in list form, directly coercable into a hash if necessary. # $r = UR::BoolExpr->resolve($c1,@p1); # ($c2, @p2) = ($r->subject_class_name, $r->params_list); my $rule_template = shift; my @values_sorted = @_; my @keys_sorted = $rule_template->_underlying_keys; my $constant_values = $rule_template->_constant_values; my @params; my ($v,$c) = (0,0); for (my $k=0; $k<@keys_sorted; $k++) { my $key = $keys_sorted[$k]; #if (substr($key,0,1) eq "_") { # next; #} #elsif (substr($key,0,1) eq '-') { if (substr($key,0,1) eq '-') { my $value = $constant_values->[$c]; push @params, $key, $value; $c++; } else { my ($property, $op) = ($key =~ /^(\-*[\w\.]+)\s*(.*)$/); unless ($property) { $DB::single = 1; Carp::confess("bad key $key in @keys_sorted"); } my $value = $values_sorted[$v]; if ($op) { if ($op ne "in") { if ($op =~ /^(.+)-(.+)$/) { $value = { operator => $1, value => $value, escape => $2 }; } else { $value = { operator => $op, value => $value }; } } } push @params, $property, $value; $v++; } } return @params; } sub _fast_construct { my ($class, $subject_class_name, # produces subject class meta $keys, # produces logic detail $constant_values, # produces constant value id $logic_detail, # optional, passed by get $constant_value_id, # optional, passed by get $subject_class_meta, # optional, passed by bx ) = @_; my $logic_type = 'And'; $logic_detail ||= join(",",@$keys); $constant_value_id ||= UR::BoolExpr::Util->values_to_value_id(@$constant_values); my $id = join('/',$subject_class_name,$logic_type,$logic_detail,$constant_value_id); my $self = $UR::Object::rule_templates->{$id}; return $self if $self; $subject_class_meta ||= $subject_class_name->__meta__; # See what properties are id-related for the class my $cache = $subject_class_meta->{cache}{'UR::BoolExpr::Template::get'} ||= do { my $id_related = {}; my $id_translations = []; my $id_pos = {}; my $id_prop_is_real; # true if there's a property called 'id' that's a real property, not from UR::Object for my $iclass ($subject_class_name, $subject_class_meta->ancestry_class_names) { last if $iclass eq "UR::Object"; next unless $iclass->isa("UR::Object"); my $iclass_meta = $iclass->__meta__; my @id_props = $iclass_meta->id_property_names; next unless @id_props; $id_prop_is_real = 1 if (grep { $_ eq 'id'} @id_props); next if @id_props == 1 and $id_props[0] eq "id" and !$id_prop_is_real; push @$id_translations, \@id_props; @$id_related{@id_props} = @id_props; @$id_pos{@id_props} = (0..$#id_props); } [$id_related,$id_translations,$id_pos]; }; my ($id_related,$id_translations,$id_pos) = @$cache; my @keys = @$keys; my @constant_values = @$constant_values; # Make a hash to quick-validate the params for duplication no warnings; my %check_for_duplicate_rules; for (my $n=0; $n < @keys; $n++) { next if (substr($keys[$n],0,1) eq '-'); my $pos = index($keys[$n],' '); if ($pos != -1) { my $property = substr($keys[$n],0,$pos); $check_for_duplicate_rules{$property}++; } else { $check_for_duplicate_rules{$keys[$n]}++; } } # each item in this list mutates the initial set of key-value pairs my $extenders = []; # add new @$extenders for class-specific characteristics # add new @keys at the same time # flag keys as removed also at the same time # note the positions for each key in the "original" rule # by original, we mean the original plus the extensions from above # my $id_position = undef; my $var_pos = 0; my $const_pos = 0; my $property_meta_hash = {}; my $property_names = []; for my $key (@keys) { if (substr($key,0,1) eq '-') { $property_meta_hash->{$key} = { name => $key, value_position => $const_pos }; $const_pos++; } else { my ($name, $op) = ($key =~ /^(.+?)\s+(.*)$/); $name ||= $key; if ($name eq 'id') { $id_position = $var_pos; } $property_meta_hash->{$name} = { name => $name, operator => $op, value_position => $var_pos }; $var_pos++; push @$property_names, $name; } } # Note whether there are properties not involved in the ID # Add value extenders for any cases of id-related properties, # or aliases. my $original_key_count = @keys; my $id_only = 1; my $partial_id = 0; my $key_op_hash = {}; if (@$id_translations and @{$id_translations->[0]} == 1) { # single-property ID ## use Data::Dumper; ## print "single property id\n". Dumper($id_translations); my ($property, $op); # Presume we are only getting id properties until another is found. # If a multi-property is partially specified, we'll zero this out too. my $values_index = -1; # -1 so we can bump it at start of loop for (my $key_pos = 0; $key_pos < $original_key_count; $key_pos++) { my $key = $keys[$key_pos]; if (substr($key, 0, 1) eq '-') { # -* are constant value keys and do not need to be changed next; } else { $values_index++; } my ($property, $op) = ($key =~ /^(.+?)\s+(.*)$/); $property ||= $key; $op ||= ""; $op =~ s/\s+//; $key_op_hash->{$property} ||= {}; $key_op_hash->{$property}{$op}++; if ($property eq "id" or $id_related->{$property}) { # Put an id key into the key list. for my $alias (["id"], @$id_translations) { next if $alias->[0] eq $property; next if $check_for_duplicate_rules{$alias->[0]}; $op ||= ""; push @keys, $alias->[0] . ($op ? " $op" : ""); push @$extenders, [ [$values_index], undef, $keys[-1] ]; $key_op_hash->{$alias->[0]} ||= {}; $key_op_hash->{$alias->[0]}{$op}++; ## print ">> extend for @$alias with op $op.\n"; } unless ($op =~ m/^(=|eq|in|\[\]|)$/) { $id_only = 0; } } elsif (substr($key,0,1) ne '-') { $id_only = 0; ## print "non id single property $property on $subject_class\n"; } } } else { # multi-property ID ## print "multi property id\n". Dumper($id_translations); my ($property, $op); my %id_parts; my $values_index = -1; # -1 so we can bump it at start of loop for (my $key_pos = 0; $key_pos < $original_key_count; $key_pos++) { my $key = $keys[$key_pos]; if (substr($key, 0, 1) eq '-') { # -* are constant value keys and do not need to be changed next; } else { $values_index++; } next if substr($key,0,1) eq '-'; my ($property, $op) = ($key =~ /^(.+?)\s+(.*)$/); $property ||= $key; $op ||= ''; $op =~ s/^\s+// if $op; $key_op_hash->{$property} ||= {}; $key_op_hash->{$property}{$op}++; if ($property eq "id") { $key_op_hash->{id} ||= {}; $key_op_hash->{id}{$op}++; # Put an id-breakdown key into the key list. for my $alias (@$id_translations) { my @new_keys = map { $_ . ($op ? " $op" : "") } @$alias; if (grep { $check_for_duplicate_rules{$_} } @new_keys) { #print "up @new_keys with @$alias\n"; } else { push @keys, @new_keys; push @$extenders, [ [$values_index], "resolve_ordered_values_from_composite_id", @new_keys ]; for (@$alias) { $key_op_hash->{$_} ||= {}; $key_op_hash->{$_}{$op}++; } # print ">> extend for @$alias with op $op.\n"; } } } elsif ($id_related->{$property}) { if ($op eq "" or $op eq "eq" or $op eq "=" or $op eq 'in') { $id_parts{$id_pos->{$property}} = $values_index; } else { # We're doing some sort of gray-area comparison on an ID # field, and though we could possibly resolve an ID # from things like an 'in' op, it's more than we've done # before. $id_only = 0; } } else { ## print "non id multi property $property on class $subject_class\n"; $id_only = 0; } } if (my $parts = (scalar(keys(%id_parts)))) { # some parts are id-related if ($parts == @{$id_translations->[0]}) { # all parts are of the id are there if (@$id_translations) { if (grep { $_ eq 'id' } @keys) { #print "found id already\n"; } else { #print "no id\n"; # we have translations of that ID into underlying properties #print "ADDING ID for " . join(",",keys %id_parts) . "\n"; my @id_pos = sort { $a <=> $b } keys %id_parts; push @$extenders, [ [@id_parts{@id_pos}], "resolve_composite_id_from_ordered_values", 'id' ]; #TODO was this correct? $key_op_hash->{id} ||= {}; $key_op_hash->{id}{$op}++; push @keys, "id"; } } } else { # not all parts of the id are there ## print "partial id property $property on class $subject_class\n"; $id_only = 0; $partial_id = 1; } } else { $id_only = 0; $partial_id = 0; } } # Determine the positions of each key in the parameter list. # In actuality, the position of the key's value in the @values or @constant_values array, # depending on whether it is a -* key or not. my %key_positions; my $vpos = 0; my $cpos = 0; for my $key (@keys) { $key_positions{$key} ||= []; if (substr($key,0,1) eq '-') { push @{ $key_positions{$key} }, $cpos++; } else { push @{ $key_positions{$key} }, $vpos++; } } # Sort the keys, and make an arrayref which will # re-order the values to match. my $last_key = ''; my @keys_sorted = map { $_ eq $last_key ? () : ($last_key = $_) } sort @keys; my $normalized_positions_arrayref = []; my $constant_value_normalized_positions = []; my $recursion_desc = undef; my $hints = undef; my $order_by = undef; my $group_by = undef; my $page = undef; my $limit = undef; my $offset = undef; my $aggregate = undef; my @constant_values_sorted; for my $key (@keys_sorted) { my $pos_list = $key_positions{$key}; my $pos = pop @$pos_list; if (substr($key,0,1) eq '-') { push @$constant_value_normalized_positions, $pos; my $constant_value = $constant_values[$pos]; if ($key eq '-recurse') { $constant_value = [$constant_value] if (!ref $constant_value); $recursion_desc = $constant_value; } elsif ($key eq '-hints' or $key eq '-hint') { $constant_value = [$constant_value] if (!ref $constant_value); $hints = $constant_value; } elsif ($key eq '-order_by' or $key eq '-order') { $constant_value = [$constant_value] if (!ref $constant_value); $order_by = $constant_value; } elsif ($key eq '-group_by' or $key eq '-group') { $constant_value = [$constant_value] if (!ref $constant_value); $group_by = $constant_value; } elsif ($key eq '-page') { $constant_value = [$constant_value] if (!ref $constant_value); $page = $constant_value; } elsif ($key eq '-limit') { $limit = $constant_value; } elsif ($key eq '-offset') { $offset = $constant_value; } elsif ($key eq '-aggregate') { $constant_value = [$constant_value] if (!ref $constant_value); $aggregate = $constant_value; } else { Carp::croak("Unknown special param '$key'. Expected one of: @UR::BoolExpr::Template::meta_param_names"); } push @constant_values_sorted, $constant_value; } else { push @$normalized_positions_arrayref, $pos; } } if ($page) { if (defined($limit) || defined($offset)) { Carp::croak("-page and -limit/-offset are mutually exclusive when defining a BoolExpr"); } if (ref($page) and ref($page) eq 'ARRAY') { if (@$page == 2) { $limit = $page->[1]; $offset = ($page->[0] - 1) * $limit; } elsif (@$page) { Carp::croak('-page must be an arrayref of two integers: -page => [$page_number, $page_size]'); } } else { Carp::croak('-page must be an arrayref of two integers: -page => [$page_number, $page_size]'); } } if (defined($hints) and ref($hints) ne 'ARRAY') { if (! ref($hints)) { $hints = [$hints]; # convert it to a list of one item } else { Carp::croak('-hints of a rule must be an arrayref of property names'); } } my $matches_all = scalar(@keys_sorted) == scalar(@constant_values); $id_only = 0 if ($matches_all); # these are used to rapidly turn a bx used for querying into one # suitable for object construction my @ambiguous_keys; my @ambiguous_property_names; for (my $n=0; $n < @keys; $n++) { next if substr($keys[$n],0,1) eq '-'; my ($property, $op) = ($keys[$n] =~ /^(.+?)\s+(.*)$/); $property ||= $keys[$n]; $op ||= ''; $op =~ s/^\s+// if $op; if ($op and $op ne 'eq' and $op ne '==') { push @ambiguous_keys, $keys[$n]; push @ambiguous_property_names, $property; } } # Determine the rule template's ID. # The normalizer will store this. Below, we'll # find or create the template for this ID. my $normalized_constant_value_id = (scalar(@constant_values_sorted) ? UR::BoolExpr::Util->values_to_value_id(@constant_values_sorted) : $constant_value_id); my @keys_unaliased = $UR::Object::Type::bootstrapping ? @keys_sorted : map { $_->[0] = substr($_->[0], 0, 1) eq '-' ? $_->[0] : $subject_class_meta->resolve_property_aliases($_->[0]); join(' ',@$_); } map { [ split(' ') ] } @keys_sorted; my $normalized_id = UR::BoolExpr::Template->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, "And", join(",",@keys_unaliased), $normalized_constant_value_id); $self = bless { id => $id, subject_class_name => $subject_class_name, logic_type => $logic_type, logic_detail => $logic_detail, constant_value_id => $constant_value_id, normalized_id => $normalized_id, # subclass specific id_position => $id_position, is_id_only => $id_only, is_partial_id => $partial_id, is_unique => undef, # assigned on first use matches_all => $matches_all, key_op_hash => $key_op_hash, _property_names_arrayref => $property_names, _property_meta_hash => $property_meta_hash, recursion_desc => $recursion_desc, hints => $hints, order_by => $order_by, group_by => $group_by, limit => $limit, offset => $offset, aggregate => $aggregate, is_normalized => ($id eq $normalized_id ? 1 : 0), normalized_positions_arrayref => $normalized_positions_arrayref, constant_value_normalized_positions_arrayref => $constant_value_normalized_positions, normalization_extender_arrayref => $extenders, num_values => scalar(@$keys), _keys => \@keys, _constant_values => $constant_values, _ambiguous_keys => (@ambiguous_keys ? \@ambiguous_keys : undef), _ambiguous_property_names => (@ambiguous_property_names ? \@ambiguous_property_names : undef), }, 'UR::BoolExpr::Template::And'; $UR::Object::rule_templates->{$id} = $self; return $self; } 1; =pod =head1 NAME UR::BoolExpr::And - a rule which is true if ALL the underlying conditions are true =head1 SEE ALSO UR::BoolExpr;(3) =cut PropertyComparison.pm000444023532023421 1171312121654173 22461 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template package UR::BoolExpr::Template::PropertyComparison; use warnings; use strict; our $VERSION = "0.41"; # UR $VERSION;; # Define the class metadata. require UR; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template'], #has => [qw/ # rule_type # subject_class_name # property_name # comparison_operator # value # resolution_code_perl # resolution_code_sql #/], #id_by => ['subject_class_name','logic_string'] ); use UR::BoolExpr::Template::PropertyComparison::Equals; use UR::BoolExpr::Template::PropertyComparison::LessThan; use UR::BoolExpr::Template::PropertyComparison::In; use UR::BoolExpr::Template::PropertyComparison::Like; sub property_name { (split(' ',$_[0]->logic_detail))[0] } sub comparison_operator { (split(' ',$_[0]->logic_detail))[1] } sub sub_group { my $self = shift; my $spec = $self->property_name; if ($spec =~ /-/) { #$DB::single = 1; } if ($spec =~ /^(.*)+\-(\w+)(\?|)(\..+|)/) { return $2 . $3; } else { return ''; } } sub get_underlying_rules_for_values { return; } sub num_values { # Not strictly correct... return 1; } sub evaluate_subject_and_values { my ($self,$subject,$comparison_value) = @_; my @property_values = $subject->__get_attr__($self->property_name); return $self->_compare($comparison_value, @property_values); } our %subclass_suffix_for_builtin_symbolic_operator = ( '=' => "Equals", '<' => "LessThan", '>' => "GreaterThan", '[]' => "In", 'in []' => "In", '!=' => "NotEqual", 'ne' => "NotEqual", '<=' => 'LessOrEqual', '>=' => 'GreaterOrEqual', ); sub resolve_subclass_for_comparison_operator { my $class = shift; my $comparison_operator = shift; # Remove any escape sequence that may have been put in at UR::BoolExpr::resolve() $comparison_operator =~ s/-.+$// if $comparison_operator; my $subclass_name; if (!defined($comparison_operator) or $comparison_operator eq '') { $subclass_name = $class . '::Equals'; } else { $comparison_operator = lc($comparison_operator); my $suffix; my $not; unless ($suffix = $subclass_suffix_for_builtin_symbolic_operator{$comparison_operator}) { my $core_comparison_operator; if ($comparison_operator =~ /not (.*)/) { $not = 1; $core_comparison_operator = $1; } elsif ($comparison_operator =~ m/between/) { $not = 0; $core_comparison_operator = 'between'; } else { $not = 0; $core_comparison_operator = $comparison_operator; } $suffix = $subclass_suffix_for_builtin_symbolic_operator{$core_comparison_operator} || ucfirst(lc($core_comparison_operator)); } $subclass_name = $class . '::' . ($not ? 'Not' : '') . $suffix; my $subclass_meta = UR::Object::Type->get($subclass_name); unless ($subclass_meta) { Carp::confess("Unknown operator '$comparison_operator'"); } } return $subclass_name; } sub _get_for_subject_class_name_and_logic_detail { my $class = shift; my $subject_class_name = shift; my $logic_detail = shift; my ($property_name, $comparison_operator) = split(' ',$logic_detail, 2); my $subclass_name = $class->resolve_subclass_for_comparison_operator($comparison_operator); my $id = $subclass_name->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, 'PropertyComparison', $logic_detail); return $subclass_name->get($id); } sub comparison_value_and_escape_character_to_regex { my ($class, $value, $escape) = @_; return '' unless defined($value); # anyone who uses the % as an escape character deserves to suffer if ($value eq '%') { return '^.+$'; } my $regex = $value; # Escape all special characters in the regex. $regex =~ s/([\(\)\[\]\{\}\+\*\.\?\|\^\$\-])/\\$1/g; # Handle the escape sequence if (defined $escape) { $escape =~ s/\\/\\\\/g; # replace \ with \\ $regex =~ s/(?define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template'], ); # sub _underlying_keys { sub get_underlying_rules_for_values { my $self = shift; my @values = @_; my @underlying_templates = $self->get_underlying_rule_templates(); my @underlying_rules; for my $template (@underlying_templates) { my $n = $template->_variable_value_count; my $rule = $template->get_rule_for_values(splice(@values,0,$n)); push @underlying_rules, $rule; } return @underlying_rules; } sub _get_for_subject_class_name_and_logic_detail { my $class = shift; my $subject_class_name = shift; my $logic_detail = shift; my $constant_id = shift; my ($logic_type) = ($class =~ /^UR::BoolExpr::Template::(.*)/); my $id = $class->__meta__->resolve_composite_id_from_ordered_values($subject_class_name, $logic_type, $logic_detail, $constant_id); return $class->get($id); } # sub get_underlying_rule_templates { # sub specifies_value_for { # evalutate_subject_and_values { 1; =pod =head1 NAME UR::BoolExpr::Composite - an "and" or "or" rule =head1 SYNOPSIS @r = $r->get_underlying_rules(); for (@r) { print $r->evaluate($c1); } =head1 DESCRIPTION =head1 SEE ALSO UR::Object(3), UR::BoolExpr, UR::BoolExpr::Template, UR::BoolExpr::Template::And, UR::BoolExpr::Template::Or, UR::BoolExpr::Template::PropertyComparison =cut PropertyComparison000755023532023421 012121654175 21745 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/TemplateNotEqual.pm000444023532023421 217612121654172 24173 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotEqual; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings 'uninitialized'; if (@property_value == 0) { return ($comparison_value eq '' ? '' : 1); } my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return '' if ( $property_value == $comparison_value ); } else { return '' if ( $property_value eq $comparison_value ); } } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotEqual - perform a not-equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns false if any if the values are equal to the comparison value =cut Like.pm000444023532023421 227512121654172 23327 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::Like; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; return '' unless defined ($comparison_value); # property like NULL should always be false my $escape = '\\'; my $regex = $class-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); no warnings 'uninitialized'; foreach my $value ( @property_value ) { return 1 if $value =~ $regex; } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Like - perform an SQL-ish like test =head1 DESCRIPTION The input test value is assummed to be an SQL 'like' value, where '_' represents a one character wildcard, and '%' means a 0 or more character wildcard. It gets converted to a perl regular expression and used to match against an object's properties. If the property returns multiple values, this comparison returns true if any of the values match. =cut True.pm000444023532023421 142612121654173 23360 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::True; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings; if (@property_value == 0) { return ''; } else { for (@property_value) { return 1 if ($_); # Returns true if _any_ of the values are true } return ''; } } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::True - Evaluates to true if the property's value is true If the property returns multiple values, this comparison returns true if any of the values are true =cut GreaterThan.pm000444023532023421 206612121654173 24646 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::GreaterThan; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value > $comparison_value ); } else { return 1 if ( $property_value gt $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::GreaterThan - perform a greater than test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are greater than the comparison value =cut False.pm000444023532023421 143212121654173 23470 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::False; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings; if (@property_value == 0) { return 1; } else { for (@property_value) { return 1 if (! $_); # Returns true if _any_ of the values are false } return ''; } } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::False - evaluates to true if the property's value is false If the property returns multiple values, this comparison returns true if any of the values are false =cut LessOrEqual.pm000444023532023421 207512121654173 24641 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::LessOrEqual; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value <= $comparison_value ); } else { return 1 if ( $property_value le $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::LessOrEqual - perform a less than or equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are less or equal to the comparison value =cut NotIn.pm000444023532023421 325412121654173 23471 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotIn; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], doc => "Returns false if any of the property's values appears in the comparison value list", ); sub _compare { my ($class,$comparison_values,@property_values) = @_; if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # undef should match missing values, which will be sorted at the end - the sorter in # UR::BoolExpr::resolve() takes care of the sorting for us if (! @property_values and !defined($comparison_values->[-1])) { return ''; } my($pv_idx, $cv_idx); no warnings; my $sorter = sub { return $property_values[$pv_idx] cmp $comparison_values->[$cv_idx] }; use warnings; # Binary search within @$comparison_values my $cv_min = 0; my $cv_max = $#$comparison_values; for ( $pv_idx = 0; $pv_idx < @property_values; $pv_idx++ ) { do { $cv_idx = ($cv_min + $cv_max) >> 1; my $result = &$sorter; if (!$result) { return ''; } elsif ($result > 0) { $cv_min = $cv_idx + 1; } else { $cv_max = $cv_idx - 1; } } until ($cv_min > $cv_max); } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotIn - perform a negated In comparison =head1 DESCRIPTION Returns false if any of the property's values appears in the comparison value list =cut Between.pm000444023532023421 250112121654173 24025 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::Between; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($self, $value, @property_value) = @_; my $lower_bound = $value->[0]; my $upper_bound = $value->[1]; my $cv_is_number = Scalar::Util::looks_like_number($lower_bound) and Scalar::Util::looks_like_number($upper_bound); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value >= $lower_bound and $property_value <= $upper_bound); } else { return 1 if ( $property_value ge $lower_bound and $property_value le $upper_bound); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Between - perform a 'between' test =head1 DESCRIPTION Evaluates to true of the property's value is between the lower and upper bounds, inclusive. If the property returns multiple values, this comparison returns true if any of the values are within the bounds. =cut In.pm000444023532023421 335012121654174 23006 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::In; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], doc => "Returns true if any of the property's values appears in the comparison value list", ); sub _compare { my ($class,$comparison_values,@property_values) = @_; if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # undef should match missing values, which will be sorted at the end - the sorter in # UR::BoolExpr::resolve() takes care of the sorting for us if (! @property_values and !defined($comparison_values->[-1])) { return 1; } my($pv_idx, $cv_idx); no warnings; my $sorter = sub { return $property_values[$pv_idx] cmp $comparison_values->[$cv_idx] }; use warnings; # Binary search within @$comparison_values for ( $pv_idx = 0; $pv_idx < @property_values; $pv_idx++ ) { my $cv_min = 0; my $cv_max = $#$comparison_values; do { $cv_idx = ($cv_min + $cv_max) >> 1; my $result = &$sorter; if (!$result) { return 1; } elsif ($result > 0) { $cv_min = $cv_idx + 1; } else { $cv_max = $cv_idx - 1; } } until ($cv_min > $cv_max); } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::In - perform an In test =head1 DESCRIPTION Returns true if any of the property's values appears in the comparison value list. Think of 'in' as short for 'intersect', and not just SQL's 'IN' operator. =cut GreaterOrEqual.pm000444023532023421 211412121654174 25317 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings qw(numeric uninitialized); foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value >= $comparison_value ); } else { return 1 if ( $property_value ge $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::GreaterOrEqual - perform a greater than or equal test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are greater or equal to the comparison value =cut LessThan.pm000444023532023421 204612121654175 24163 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::LessThan; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($cv_is_number and $pv_is_number) { return 1 if ( $property_value < $comparison_value ); } else { return 1 if ( $property_value lt $comparison_value ); } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::LessThan - perform a less than test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are less than the comparison value =cut NotLike.pm000444023532023421 222312121654175 24004 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::NotLike; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; my $escape = '\\'; my $regex = $class-> comparison_value_and_escape_character_to_regex( $comparison_value, $escape ); no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { return '' if ($property_value =~ $regex); } return 1; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::NotLike - perform a negated SQL-ish like test =head1 DESCRIPTION The input test value is assummed to be an SQL 'like' value, where '_' represents a one character wildcard, and '%' means a 0 or more character wildcard. It gets converted to a perl regular expression and used in a negated match against an object's properties If the property returns multiple values, this comparison returns false if any of the values matches the pattern =cut Matches.pm000444023532023421 135212121654175 24025 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparison package UR::BoolExpr::Template::PropertyComparison::Matches; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_value) = @_; no warnings 'uninitialized'; foreach my $property_value ( @property_value ) { return 1 if ( $property_value =~ m/$comparison_value/ ); } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Matches - perform a Perl regular expression match =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values match =cut Equals.pm000444023532023421 222412121654175 23672 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/BoolExpr/Template/PropertyComparisonpackage UR::BoolExpr::Template::PropertyComparison::Equals; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::BoolExpr::Template::PropertyComparison'], ); sub _compare { my ($class,$comparison_value,@property_values) = @_; no warnings 'uninitialized'; if (@property_values == 0) { return ($comparison_value eq '' ? 1 : ''); } no warnings 'numeric'; my $cv_is_number = Scalar::Util::looks_like_number($comparison_value); foreach my $property_value ( @property_values ) { my $pv_is_number = Scalar::Util::looks_like_number($property_value); if ($pv_is_number and $cv_is_number) { return 1 if $property_value == $comparison_value; } else { return 1 if $property_value eq $comparison_value; } } return ''; } 1; =pod =head1 NAME UR::BoolExpr::Template::PropertyComparison::Equals - perform a strictly equals test =head1 DESCRIPTION If the property returns multiple values, this comparison returns true if any of the values are equal to the comparison value =cut Doc000755023532023421 012121654175 13266 5ustar00abrummetgsc000000000000UR-0.41/lib/URWriter.pm000444023532023421 133412121654173 15234 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Docpackage UR::Doc::Writer; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use Carp qw/croak/; class UR::Doc::Writer { is => 'UR::Object', is_abstract => 1, has => [ title => { is => 'Text', }, sections => { is => 'UR::Doc::Section', is_many => 1, }, navigation => { is => 'ARRAY', is_optional => 1, }, ], has_transient_optional => [ content => { is => 'Text', default_value => '', }, ] }; sub _append { my ($self, $data) = @_; $self->content($self->content . $data); } sub generate_index { my ($self, @command_trees) = @_; return ''; } Section.pm000444023532023421 77712121654173 15356 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Docpackage UR::Doc::Section; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; class UR::Doc::Section { is => 'UR::Object', has => [ title => { is => 'Text', is_optional => 1, }, content => { is => 'Text', doc => 'pod content for this section', }, format => { is => 'Text', default_value => 'pod', valid_values => ['html','pod','txt'], }, ], }; Pod2Html.pm000444023532023421 101612121654175 15410 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Docpackage UR::Doc::Pod2Html; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; use Data::Dumper; use parent 'Pod::Simple::HTML'; $Pod::Simple::HTML::Perldoc_URL_Prefix = ''; $Pod::Simple::HTML::Perldoc_URL_Postfix = '.html'; sub do_top_anchor { my ($self, $value) = @_; $self->{__do_top_anchor} = $value; } sub do_beginning { return 1; } sub do_end { return 1; } sub _add_top_anchor { my $self = shift; return $self->SUPER::_add_top_anchor(@_) if $self->{__do_top_anchor}; } 1; Writer000755023532023421 012121654173 14540 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DocHtml.pm000444023532023421 561212121654172 16142 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Doc/Writerpackage UR::Doc::Writer::Html; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use UR::Doc::Section; use UR::Doc::Pod2Html; use Carp qw/croak/; class UR::Doc::Writer::Html { is => 'UR::Doc::Writer', }; sub render { my $self = shift; $self->content(''); $self->_render_header; $self->_render_index; my $i = 0; for my $section ($self->sections) { $self->_render_section($section, $i++); } $self->_render_footer; } sub _render_header { my $self = shift; if ($self->navigation) { my @nav_html; for my $item (@{$self->navigation}) { my ($name, $uri) = @$item; if ($uri) { push(@nav_html, "$name"); } else { push(@nav_html, $name); } } $self->_append(join(" :: ", @nav_html) . "
\n"); } my $translator = new UR::Doc::Pod2Html; my $title; $translator->output_string($title); $translator->parse_string_document("=pod\n\n".$self->title."\n\n=cut\n\n"); $self->_append("

$title

\n"); } sub _render_index { my $self = shift; my @titles = grep { $_ and /./ } map { $_->title } $self->sections; my $i = 0; if (@titles) { $self->_append("\n
    \n". join("\n", map {"
  • $_
  • "} @titles)."
\n\n"); } } sub _render_section { my ($self, $section, $idx) = @_; if (my $title = $section->title) { $self->_append("

$title

\n"); } my $content = $section->content; if ($section->format eq 'html') { $self->_append($content); } elsif ($section->format eq 'txt' or $section->format eq 'pod') { $content = "\n\n=pod\n\n$content\n\n=cut\n\n"; my $new_content; my $translator = new UR::Doc::Pod2Html; $translator->output_string($new_content); $translator->parse_string_document($content); $self->_append($new_content); } else { croak "Unknown section type " . $section->type; } $self->_append("
\n"); } sub _render_footer { my $self = shift; $self->_append(""); } sub generate_index { my ($self, @command_trees) = @_; return '' unless @command_trees; my $html = "

Command Index


\n"; $html .= $self->_generate_index_body(@command_trees); return $html; } sub _generate_index_body { my ($self, @command_trees) = @_; return '' unless @command_trees; my $html = "
    \n"; for my $tree (@command_trees) { my $name = $tree->{command_name_brief}; my $uri = $tree->{uri}; $html .= "
  • $name\n"; $html .= $self->_generate_index_body(@{$tree->{sub_commands}}); $html .= "
  • \n"; } $html .= "
\n"; return $html; } 1; Pod.pm000444023532023421 226712121654173 15764 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Doc/Writerpackage UR::Doc::Writer::Pod; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; use UR::Doc::Section; use Carp qw/croak/; class UR::Doc::Writer::Pod { is => 'UR::Doc::Writer', }; sub render { my $self = shift; $self->content(''); $self->_render_header; $self->_render_index; map { $self->_render_section($_) } $self->sections; $self->_render_footer; return $self->content; } sub _render_header { my $self = shift; $self->_append("\n\n=pod\n\n"); if (my $title = $self->title) { $self->_append("=head1 $title\n\n"); } } sub _render_index { # no indexing for pod } sub _render_section { my ($self, $section) = @_; my $title = $section->title; $self->_append("=head1 $title\n") if $title; my $content = $section->content; if ($section->format eq 'html') { $self->warning_message("Skipping html section '$title' while rendering pod"); } elsif ($section->format eq 'txt' or $section->format eq 'pod') { $self->_append("\n\n=pod\n\n$content\n\n=cut\n\n"); } else{ croak "Unknown section type " . $section->type; } } sub _render_footer { my $self = shift; } 1; DataSource000755023532023421 012121654175 14613 5ustar00abrummetgsc000000000000UR-0.41/lib/URFileMux.pm000444023532023421 7306612121654172 16710 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::FileMux; # NOTE! This module is deprecated. Use UR::DataSource::Filesystem instead. use UR; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; class UR::DataSource::FileMux { is => ['UR::DataSource'], doc => 'A factory for other datasource factories that is able to pivot depending on parameters in the rule used for get()', has => [ delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, column_order => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, cache_size => { is => 'Integer', default_value => 100 }, skip_first_line => { is => 'Integer', default_value => 0 }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, quick_disconnect => { is => 'Boolean', default_value => 1, doc => 'Do not hold the file handle open between requests' }, file_resolver => { is => 'CODE', doc => 'subref that will return a pathname given a rule' }, constant_values => { is => 'ARRAY', default_value => undef, doc => 'Property names which are not in the data file(s), but are part of the objects loaded from the data source' }, ], has_optional => [ server => { is => 'String', doc => 'pathname to the data file' }, file_list => { is => 'ARRAY', doc => 'list of pathnames of equivalent files' }, sort_order => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, required_for_get => { is => 'ARRAY', doc => 'Property names which must appear in any get() request using this data source. It is used to build the argument list for the file_resolver sub' }, ], }; # The concreate data sources will be of this type sub _delegate_data_source_class { 'UR::DataSource::File'; } sub sql_fh { return UR::DBI->sql_fh(); } sub can_savepoint { 0;} # Doesn't support savepoints my %WORKING_RULES; # Avoid recusion when infering values from rules sub create_iterator_closure_for_rule { my($self,$rule) = @_; if ($WORKING_RULES{$rule->id}++) { my $subject_class = $rule->subject_class_name; $self->error_message("Recursive entry into create_iterator_closure_for_rule() for class $subject_class rule_id ".$rule->id); $WORKING_RULES{$rule->id}--; return; } my $context = UR::Context->get_current; my $required_for_get = $self->required_for_get; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->printf("FILEMux: Resolving values for %d params (%s)\n", scalar(@$required_for_get), join(',',@$required_for_get)); } my @all_resolver_params; for(my $i = 0; $i < @$required_for_get; $i++) { my $param_name = $required_for_get->[$i]; my @values = $context->infer_property_value_from_rule($param_name, $rule); unless (@values) { # Hack: the above infer...rule() returned 0 objects, so $all_params_loaded made # a note of it. Later on, if the user supplies more params such that it would be # able to resolve a file, we'll never get here, because the Context will see that a # superset of the params (this current invocation without sufficient params) was already # tried and results should be entirely in the cache - ie. no objects. # So... remove the evidence that we tried this in case the user is catching the die # below and will continue on $context->_forget_loading_was_done_with_template_and_rule($rule->template_id, $rule->id); Carp::croak "Can't resolve data source: no $param_name specified in rule $rule"; } if (@values == 1 and ref($values[0]) eq 'ARRAY') { @values = @{$values[0]}; } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print(" FILEMux: $param_name: (",join(',',@values),")\n"); } unless ($rule->specifies_value_for($param_name)) { if (scalar(@values) == 1) { $rule = $rule->add_filter($param_name => $values[0]); } else { $rule = $rule->add_filter($param_name => \@values); } } $all_resolver_params[$i] = \@values; } my @resolver_param_combinations = UR::Util::combinations_of_values(@all_resolver_params); # Each combination of params ends up being from a different data source. Make an # iterator pulling from each of them my $file_resolver = $self->{'file_resolver'}; if (ref($file_resolver) ne 'CODE') { # Hack! The data source is probably a singleton class and there's a file_resolver method # defined $file_resolver = $self->can('file_resolver'); } my $concrete_ds_type = $self->_delegate_data_source_class; #my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); my @constant_value_properties = @{$self->constant_values}; my @data_source_construction_data; foreach my $resolver_params ( @resolver_param_combinations ) { push @data_source_construction_data, { subject_class_name => $rule->subject_class_name, file_resolver => $file_resolver, file_resolver_params => $resolver_params, }; } delete $WORKING_RULES{$rule->id}; my($monitor_start_time,$monitor_printed_first_fetch); if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); $monitor_printed_first_fetch = 0; } my $base_sub_ds_name = $self->id; # Fill in @ds_iterators with iterators for all the underlying data sources # pre-fill @ds_next_row with the next object from each data source # @ds_constant_values is the constant_values for objects of those data sources my(@ds_iterators, @ds_next_row, @ds_constant_values); foreach my $data_source_construction_data ( @data_source_construction_data ) { my $subject_class_name = $data_source_construction_data->{'subject_class_name'}; my $file_resolver = $data_source_construction_data->{'file_resolver'}; my $file_resolver_params = $data_source_construction_data->{'file_resolver_params'}; my @sub_ds_name_parts; my $this_ds_rule_params = $rule->legacy_params_hash; for (my $i = 0; $i < @$required_for_get; $i++) { my $param_name = $required_for_get->[$i]; my $param_value = $file_resolver_params->[$i]; push @sub_ds_name_parts, $param_name . $param_value; $this_ds_rule_params->{$param_name} = $param_value; } my $sub_ds_id = join('::', $base_sub_ds_name, @sub_ds_name_parts); my $resolved_file = $file_resolver->(@$file_resolver_params); unless ($resolved_file) { Carp::croak "Can't create data source: file resolver for $sub_ds_id returned false for params " . join(',',@$file_resolver_params); } my $this_ds_obj = $self->get_or_create_data_source($concrete_ds_type, $sub_ds_id, $resolved_file); my $this_ds_rule = UR::BoolExpr->resolve($subject_class_name,%$this_ds_rule_params); my @constant_values = map { $this_ds_rule->value_for($_) } @constant_value_properties; my $ds_iterator = $this_ds_obj->create_iterator_closure_for_rule($this_ds_rule); my $initial_obj = $ds_iterator->(); next unless $initial_obj; push @ds_constant_values, \@constant_values; push @ds_iterators, $ds_iterator; push @ds_next_row, $initial_obj; } unless (scalar(@ds_constant_values) == scalar(@ds_iterators) and scalar(@ds_constant_values) == scalar(@ds_next_row) ) { Carp::croak("Internal error in UR::DataSource::FileMux: arrays for iterators, constant_values and next_row have differing sizes"); } # Create a closure that can sort the next possible rows in @ds_next_row and return the index of # the one that sorts earliest my $sorter; if (@ds_iterators == 0 ) { # No underlying data sources, no data to return return sub {}; } elsif (@ds_iterators == 1 ) { # Only one underlying data source. $sorter = sub { 0 }; } else { # more than one underlying data source, make a real sorter my %column_name_to_row_index; my $column_order_names = $self->column_order; my $constant_values = $self->constant_values; push @$column_order_names, @$constant_values; for (my $i = 0; $i < @$column_order_names; $i++) { $column_name_to_row_index{$column_order_names->[$i]} = $i; } my $sort_order = $self->sort_order; if (! $sort_order or ! @$sort_order ) { # They didn't specify sorting, Try finding out the class' ID properties # and sort by them my $subject_class_meta = $rule->subject_class_name->__meta__; my @id_properties = $subject_class_meta->direct_id_property_names; $sort_order = []; foreach my $property_name ( @id_properties ) { my $property_meta = $subject_class_meta->property_meta_for_name($property_name); my $column_name = $property_meta->column_name; next unless $column_name; next unless ($column_name_to_row_index{$column_name}); push @$sort_order, $column_name; } } my @row_index_sort_order = map { $column_name_to_row_index{$_} } @$sort_order; $sorter = sub { my $lowest_obj_idx = 0; COMPARE_OBJECTS: for(my $compare_obj_idx = 1; $compare_obj_idx < @ds_next_row; $compare_obj_idx++) { COMPARE_COLUMNS: for (my $i = 0; $i < @row_index_sort_order; $i++) { my $column_num = $row_index_sort_order[$i]; my $comparison = $ds_next_row[$lowest_obj_idx]->[$column_num] <=> $ds_next_row[$compare_obj_idx]->[$column_num] || $ds_next_row[$lowest_obj_idx]->[$column_num] cmp $ds_next_row[$compare_obj_idx]->[$column_num]; if ($comparison == -1) { next COMPARE_OBJECTS; } elsif ($comparison == 1) { $lowest_obj_idx = $compare_obj_idx; next COMPARE_OBJECTS; } } } return $lowest_obj_idx; }; } my $iterator = sub { if ($monitor_start_time and ! $monitor_printed_first_fetch) { $self->sql_fh->printf("FILEMux: FIRST FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); $monitor_printed_first_fetch = 1; } while (@ds_next_row) { my $next_row_idx = $sorter->(); my $next_row_to_return = $ds_next_row[$next_row_idx]; push @$next_row_to_return, @{$ds_constant_values[$next_row_idx]}; my $refill_row = $ds_iterators[$next_row_idx]->(); if ($refill_row) { $ds_next_row[$next_row_idx] = $refill_row; } else { # This iterator is exhausted splice(@ds_iterators, $next_row_idx, 1); splice(@ds_constant_values, $next_row_idx, 1); splice(@ds_next_row, $next_row_idx, 1); } return $next_row_to_return; } if ($monitor_start_time) { $self->sql_fh->printf("FILEMux: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return; }; Sub::Name::subname('UR::DataSource::FileMux::__datasource_iterator(closure)__', $iterator); return $iterator; } sub get_or_create_data_source { my($self, $concrete_ds_type, $sub_ds_id, $file_path) = @_; my $sub_ds; unless ($sub_ds = $concrete_ds_type->get($sub_ds_id)) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print("FILEMux: $file_path is data source $sub_ds_id\n"); } my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); $concrete_ds_type->define( id => $sub_ds_id, %sub_ds_params, server => $file_path, ); $UR::Context::all_objects_cache_size++; $sub_ds = $concrete_ds_type->get($sub_ds_id); unless ($sub_ds) { Carp::croak "Can't create data source: retrieving newly defined data source $sub_ds_id returned nothing"; } # Since these $sub_ds objects have no data_source, this will indicate to # UR::Context::prune_object_cache() that it's ok to go ahead and drop them $sub_ds->__weaken__(); } return $sub_ds; } sub _generate_loading_templates_arrayref { my $self = shift; my $delegate_class = $self->_delegate_data_source_class(); $delegate_class->class; # trigger the autoloader, if necessary my $function_name = $delegate_class . '::_generate_loading_templates_arrayref'; no strict 'refs'; return &$function_name($self,@_); } sub _normalize_file_resolver_details { my($class, $class_data, $ds_data) = @_; my $path_resolver_coderef; my @required_for_get; my $class_name = $class_data->{'class_name'}; if (exists $ds_data->{'required_for_get'}) { @required_for_get = @{$ds_data->{'required_for_get'}}; my $user_supplied_resolver = $ds_data->{'file_resolver'} || $ds_data->{'resolve_file_with'} || $ds_data->{'resolve_path_with'}; if (ref($user_supplied_resolver) eq 'CODE') { $path_resolver_coderef = $user_supplied_resolver; } elsif (! ref($user_supplied_resolver)) { # It's a functcion name $path_resolver_coderef = $class_name->can($user_supplied_resolver); unless ($path_resolver_coderef) { die "Can't locate function $user_supplied_resolver via class $class_name during creation of inline data source"; } } else { $class->error_message("The data_source specified 'required_for_get', but the file resolver was not a coderef or function name"); return; } } else { my $resolve_path_with = $ds_data->{'resolve_path_with'} || $ds_data->{'path'} || $ds_data->{'server'} || $ds_data->{'file_resolver'}; unless ($resolve_path_with or $ds_data->{'file_list'}) { $class->error_message("A data_source's definition must include 'resolve_path_with', 'path', 'server', or 'file_list'"); return; } if (! ref($resolve_path_with)) { # a simple string if ($class_name->can($resolve_path_with) or grep { $_ eq $resolve_path_with } @{$class_data->{'has'}}) { # a method or property name no strict 'refs'; $path_resolver_coderef = \&{ $class_name . "::$resolve_path_with"}; } else { # a hardcoded pathname $path_resolver_coderef = sub { $resolve_path_with }; } } elsif (ref($resolve_path_with) eq 'CODE') { $path_resolver_coderef = $resolve_path_with; } elsif (ref($resolve_path_with) ne 'ARRAY') { $class->error_message("A data_source's 'resolve_path_with' must be a coderef, arrayref, pathname or method name"); return; } elsif (ref($resolve_path_with) eq 'ARRAY') { # A list of things if (ref($resolve_path_with->[0]) eq 'CODE') { # A coderef, then property list @required_for_get = @{$ds_data->{'resolve_path_with'}}; $path_resolver_coderef = shift @required_for_get; } elsif (grep { $_ eq $resolve_path_with->[0] } keys(%{$class_data->{'has'}}) ) { # a list of property names, join them with /s unless ($ds_data->{'base_path'}) { $class->warning_message("$class_name inline data source: 'resolve_path_with' is a list of method names, but 'base_path' is undefined'"); } @required_for_get = @{$resolve_path_with}; my $base_path = $ds_data->{'base_path'}; $path_resolver_coderef = sub { no warnings 'uninitialized'; return join('/', $base_path, @_) }; } elsif ($class_name->can($resolve_path_with->[0])) { # a method compiled into the class, but not one that's a property @required_for_get = @{$resolve_path_with}; my $fcn_name = shift @required_for_get; my $path_resolver_coderef = $class_name->can($fcn_name); unless ($path_resolver_coderef) { die "Can't locate function $fcn_name via class $class_name during creation of inline data source"; } } elsif (! ref($resolve_path_with->[0])) { # treat the first element as a sprintf format @required_for_get = @{$resolve_path_with}; my $format = shift @required_for_get; $path_resolver_coderef = sub { no warnings 'uninitialized'; return sprintf($format, @_); }; } else { $class->error_message("Unrecognized layout for 'resolve_path_with'"); return; } } else { $class->error_message("Unrecognized layout for 'resolve_path_with'"); return; } } return ($path_resolver_coderef, @required_for_get); } # Properties we'll copy from $self when creating a concrete data source sub _common_params_for_concrete_data_sources { my $self = shift; my %params; foreach my $param ( qw( delimiter skip_first_line column_order sort_order record_separator constant_values handle_class quick_disconnect ) ) { next unless defined $self->$param; my @vals = $self->$param; if (@vals > 1) { $params{$param} = \@vals; } else { $params{$param} = $vals[0]; } } return %params; } sub initializer_should_create_column_name_for_class_properties { 1; } # Called by the class initializer sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; unless ($ds_data->{'column_order'}) { die "Can't create inline data source for ".$class_data->{'class_name'}.": 'column_order' is a required param"; } my($file_resolver, @required_for_get) = $class->_normalize_file_resolver_details($class_data, $ds_data); return unless $file_resolver; if (!exists($ds_data->{'constant_values'}) and @required_for_get) { # If there are required_for_get params, but the user didn't specify any constant_values, # then all the required_for_get items that are real properties become constant_values $ds_data->{'constant_values'} = []; my %columns_from_ds = map { $_ => 1 } @{$ds_data->{'column_order'}}; foreach my $param_name ( @required_for_get ) { my $param_data = $class_data->{'has'}->{$param_name}; next unless $param_data; my $param_column = $param_data->{'column_name'}; next unless $param_column; unless ($columns_from_ds{$param_column}) { push @{$ds_data->{'constant_values'}}, $param_name; } } } my %ds_creation_params; foreach my $param ( qw( delimiter record_separator column_order cache_size skip_first_line sort_order constant_values ) ) { if (exists $ds_data->{$param}) { $ds_creation_params{$param} = $ds_data->{$param}; } } my($namespace, $class_name) = ($class_data->{'class_name'} =~ m/^(\w+?)::(.*)/); my $ds_id = "${namespace}::DataSource::${class_name}"; my $ds_type = delete $ds_data->{'is'}; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id, required_for_get => \@required_for_get, file_resolver => $file_resolver ); return $ds; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $changed_objects = delete $params{'changed_objects'}; my $context = UR::Context->get_current; my $required_for_get = $self->required_for_get; my $file_resolver = $self->{'file_resolver'}; if (ref($file_resolver) ne 'CODE') { # Hack! The data source is probably a singleton class and there's a file_resolver method # defined $file_resolver = $self->can('file_resolver'); } my $monitor_start_time; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); my $time = time(); $self->sql_fh->printf("FILEMux: SYNC_DATABASE AT %d [%s].\n", $time, scalar(localtime($time))); } my $concrete_ds_type = $self->_delegate_data_source_class; my %sub_ds_params = $self->_common_params_for_concrete_data_sources(); my %datasource_for_dsid; my %objects_by_datasource; foreach my $obj ( @$changed_objects ) { my @obj_values; for (my $i = 0; $i < @$required_for_get; $i++) { my $property = $required_for_get->[$i]; my $value = $obj->$property; unless ($value) { my $class = $obj->class; my $id = $obj->id; $self->error_message("No value for required-for-get property $property on object of class $class id $id"); return; } if (ref $value) { my $class = $obj->class; my $id = $obj->id; $self->error_message("Pivoting based on a non-scalar property is not supported. $class object id $id property $property did not return a scalar value"); return; } push @obj_values, $value; } my @sub_ds_name_parts; for (my $i = 0; $i < @obj_values; $i++) { push @sub_ds_name_parts, $required_for_get->[$i] . $obj_values[$i]; } my $sub_ds_id = join('::', $self->id, @sub_ds_name_parts); my $sub_ds = $datasource_for_dsid{$sub_ds_id} || $concrete_ds_type->get($sub_ds_id); unless ($sub_ds) { my $file_path = $file_resolver->(@obj_values); unless (defined $file_path) { die "Can't resolve data source: resolver for " . $self->class . " returned undef for params " . join(',',@obj_values); } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->print("FILEMux: $file_path is data source $sub_ds_id\n"); } $concrete_ds_type->define( id => $sub_ds_id, %sub_ds_params, server => $file_path, ); $UR::Context::all_objects_cache_size++; $sub_ds = $concrete_ds_type->get($sub_ds_id); # Since these $sub_ds objects have no data_source, this will indicate to # UR::Context::prune_object_cache() that it's ok to go ahead and drop them $sub_ds->__weaken__(); } unless ($sub_ds) { die "Can't get data source with ID $sub_ds_id"; } $datasource_for_dsid{$sub_ds_id} ||= $sub_ds; unless ($objects_by_datasource{$sub_ds_id}) { $objects_by_datasource{$sub_ds_id}->{'ds_obj'} = $sub_ds; $objects_by_datasource{$sub_ds_id}->{'changed_objects'} = []; } push(@{$objects_by_datasource{$sub_ds_id}->{'changed_objects'}}, $obj); } foreach my $h ( values %objects_by_datasource ) { my $sub_ds = $h->{'ds_obj'}; my $changed_objects = $h->{'changed_objects'}; $sub_ds->_sync_database(changed_objects => $changed_objects); } if ($ENV{'UR_DBI_MONITOR_SQL'}) { $self->sql_fh->printf("FILEMux: TOTAL COMMIT TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return 1; } 1; =pod =head1 NAME UR::DataSource::FileMux - Parent class for datasources which can multiplex many files together =head1 DEPRECATED This module is deprecated. Use UR::DataSource::Filesystem instead. =head1 SYNOPSIS package MyNamespace::DataSource::MyFileMux; class MyNamespace::DataSource::MyFileMux { is => ['UR::DataSource::FileMux', 'UR::Singleton'], }; sub column_order { ['thing_id', 'thing_name', 'thing_color'] } sub sort_order { ['thing_id'] } sub delimiter { "\t" } sub constant_values { ['thing_type'] } sub required_for_get { ['thing_type'] } sub file_resolver { my $thing_type = shift; return '/base/path/to/files/' . $thing_type; } package main; class MyNamespace::ThingMux { id_by => ['thing_id', 'thing_type' ], has => ['thing_id', 'thing_type', 'thing_name','thing_color'], data_source => 'MyNamespace::DataSource::MyFileMux', }; my @objs = MyNamespace::Thing->get(thing_type => 'people', thing_name => 'Bob'); =head1 DESCRIPTION UR::DataSource::FileMux provides a framework for file-based data sources where the data files are split up between one or more parameters of the class. For example, in the synopsis above, the data for the class is stored in several files in the directory /base/path/to/files/. Each file may have a name such as 'people' and 'cars'. When a get() request is made on the class, the parameter 'thing_type' must be present in the rule, and the value of that parameter is used to complete the file's pathname, via the file_resolver() function. Note that even though the 'thing_type' parameter is not actually stored in the file, its value for the loaded objects gets filled in because that paremeter exists in the constant_values() configuration list, and in the get() request. =head2 Configuration These methods determine the configuration for your data source and should appear as properties of the data source or as functions in the package. =over 4 =item delimiter() =item record_separator() =item skip_first_line() =item column_order() =item sort_order() These configuration items behave the same as in a UR::DataSource::File-based data source. =item required_for_get() required_for_get() should return a listref of parameter names. Whenever a get() request is made on the class, the listed parameters must appear in the rule, or be derivable via UR::Context::infer_property_value_from_rule(). =item file_resolver() file_resolver() is called as a function (not a method). It should accept the same number of parameters as are mentioned in required_for_get(). When a get() request is made, those named parameters are extracted from the rule and passed in to the file_resolver() function in the same order. file_resolver() must return a string that is used as the pathname to the file that contains the needed data. The function must not have any other side effects. In the case where the data source is a regular object (not a UR::Singleton'), then the file_resover parameter should return a coderef. =item constant_values() constant_values() should return a listref of parameter names. These parameter names are used by the object loader system to fill in data that may not be present in the data files. If the class has parameters that are not actually stored in the data files, then the parameter values are extracted from the rule and stored in the loaded object instances before being returned to the user. In the synopsis above, thing_type is not stored in the data files, even though it exists as a parameter of the MyNamespace::ThingMux class. =back =head2 Theory of Operation As part of the data-loading infrastructure inside UR, the parameters in a get() request are transformed into a UR::BoolExpr instance, also called a rule. UR::DataSource::FilMux hooks into that infrastructure by implementing create_iterator_closure_for_rule(). It first collects the values for all the parameters mentioned in required_for_get() by passing the rule and needed parameter to infer_property_value_from_rule() of the current Context. If any of the needed parameters is not resolvable, an excpetion is raised. Some of the rule's parameters may have multiple values. In those cases, all the combinations of values are expanded. For example of param_a has 2 values, and param_b has 3 values, then there are 6 possible combinations. For each combination of values, the file_resolver() function is called and returns a pathname. For each pathname, a file-specific data source is created (if it does not already exist), the server() configuration parameter created to return that pathname. Other parameters are copied from the values in the FileMux data source, such as column_names and delimiter. create_iterator_closure_for_rule() is called on each of those data sources. Finally, an iterator is created to wrap all of those iterators, and is returned. =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource, UR::DataSource::File =cut Meta.pm000444023532023421 1311712121654172 16214 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Meta', is => ['UR::DataSource::SQLite'], ); sub _resolve_class_name_for_table_name_fixups { my $self = shift->_singleton_object; if ($_[0] =~ m/Dd/) { $_[0] = "DataSource::RDBMS::"; } return @_; } # Do a DB dump at commit time sub dump_on_commit { 1; } # This is the template for the schema: our $METADATA_DB_SQL =<__meta__->module_path(); my $meta_datasource_name = $namespace_name . '::DataSource::Meta'; my $meta_datasource = UR::Object::Type->define( class_name => $meta_datasource_name, is => 'UR::DataSource::Meta', is_abstract => 0, ); my $meta_datasource_src = $meta_datasource->resolve_module_header_source(); my $meta_datasource_filename = $meta_datasource->module_base_name(); my $meta_datasource_filepath = $namespace_path; return unless defined($meta_datasource_filepath); # This namespace could be fabricated at runtime $meta_datasource_filepath =~ s/.pm//; $meta_datasource_filepath .= '/DataSource'; mkdir($meta_datasource_filepath); unless (-d $meta_datasource_filepath) { die "Failed to create directory $meta_datasource_filepath: $!"; } $meta_datasource_filepath .= '/Meta.pm'; # Write the Meta DB datasource Module if (-e $meta_datasource_filepath) { Carp::croak("Can't create new MetaDB datasource Module $meta_datasource_filepath: File already exists"); } my $fh = IO::File->new("> $meta_datasource_filepath"); unless ($fh) { Carp::croak("Can't create MetaDB datasource Module $meta_datasource_filepath: $!"); } $fh->printf($module_template, $meta_datasource_name, $meta_datasource_src); # Write the skeleton SQLite file my $meta_db_file = $meta_datasource->class_name->_data_dump_path; IO::File->new(">$meta_db_file")->print($UR::DataSource::Meta::METADATA_DB_SQL); return ($meta_datasource, $meta_db_file); } 1; =pod =head1 NAME UR::DataSource::Meta - Data source for the MetaDB =head1 SYNOPSIS my $meta_table = UR::DataSource::RDBMS::Table->get( table_name => 'DD_TABLE' namespace => 'UR', ); my @myapp_tables = UR::DataSource::RDBMS::Table->get( namespace => 'MyApp', ); =head1 DESCRIPTION UR::DataSource::Meta a datasource that contains all table/column meta data for the UR namespace itself. Essentially the schema schema. =head1 INHERITANCE UR::DataSource::Meta is a subclass of L =head1 get() required parameters C or C are required parameters when calling C on any MetaDB-sourced object types. =cut ValueDomain.pm000444023532023421 112012121654173 17502 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::ValueDomain; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::ValueDomain', is => ['UR::DataSource'], is_abstract => 1, properties => [ ], doc => 'A logical DBI-based database, independent of prod/dev/testing considerations or login details.', ); sub get_objects_for_rule { my $class = shift; my $rule = shift; my $obj = $UR::Context::current->_construct_object($rule); $obj->__signal_change__("define"); return $obj; } 1; Code.db000444023532023421 1200012121654173 16140 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceSQLite format 3@  7%7 K/)indexmethod_name_usage_i_method_namemethod_name_usageCREATE INDEX method_name_usage_i_method_name on method_name_usage(method_name)^5}indexmethod_i_method_namemethodCREATE INDEX method_i_method_name on method(method_name)G//=tablemethod_name_usagemethod_name_usageCREATE TABLE method_name_usage ( file_name varchar2(255), method_name varchar2(255), line_number integer )wtablemethodmethodCREATE TABLE method ( file_name varchar2(255), class_name varchar2(255), method_name varchar2(255), line_number integer, line_count integer, is_deprecated bool )    Meta.sqlite3-bak000444023532023421 7600012121654173 17720 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceSQLite format 3@ =Q+indexsqlite_autoindex_dd_table_column_1dd_table_column ++Qtabledd_bitmap_indexdd_bitmap_indexCREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) )=Q+indexsqlite_autoindex_dd_bitmap_index_1dd_bitmap_indexctabledd_tabledd_tableCREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) )/Cindexsqlite_autoindex_dd_table  hs.V 5C3UR::DataSource::Metadd_unique_constraint_columnTABLEentity2007-04-16 19:35:06C 53UR::DataSource::Metadd_tableTABLEentity2007-04-16 19:35:07R 5;3UR::DataSource::Metadd_fk_constraint_columnTABLEentity2007-04-16 19:35:07K 5-3UR::DataSource::Metadd_fk_constraintTABLEentity2007-04-16 19:35:07R 5;3UR::DataSource::Metadd_pk_constraint_columnTABLEentity2007-04-16 19:35:07J 5+3UR::DataSource::Metadd_bitmap_indexTABLEentity2007-04-16 19:35:06J 5+3UR::DataSource::Metadd_table_columnTABLEentity2007-04-16 19:35:06 Oz55CUR::DataSource::Metadd_unique_constraint_column"5UR::DataSource::Metadd_table15;UR::DataSource::Metadd_fk_constraint_column*5-UR::DataSource::Metadd_fk_constraint15;UR::DataSource::Metadd_pk_constraint_column)5+UR::DataSource::Metadd_bitmap_index)5+UR::DataSource::Metadd_table_column  K9Z 5;!3 UR::DataSource::Metadd_pk_constraint_columntable_namevarcharN2007-04-16 19:35:07_ 5C#3 UR::DataSource::Metadd_unique_constraint_columncolumn_namevarcharN2007-04-16 19:35:06S 5+#3 UR::DataSource::Metadd_table_columndata_lengthvarcharY2007-04-16 19:35:06c 5C+3 UR::DataSource::Metadd_unique_constraint_columnconstraint_namevarcharN2007-04-16 19:35:06H 53 UR::DataSource::Metadd_tableer_typevarcharN2007-04-16 19:35:07U 5;3 UR::DataSource::Metadd_fk_constraint_columnownervarcharN2007-04-16 19:35:07T 5;3 UR::DataSource::Metadd_pk_constraint_columnrankintegerN2007-04-16 19:35:07U 5;3 UR::DataSource::Metadd_pk_constraint_columnownervarcharY2007-04-16 19:35:07S 5+#3 UR::DataSource::Metadd_bitmap_indexdata_sourcevarcharN2007-04-16 19:35:06R 5+!3 UR::DataSource::Metadd_bitmap_indextable_namevarcharN2007-04-16 19:35:06W 553 UR::DataSource::Metadd_tablelast_object_revisiontimestamp,!  %Q6E25+UR::DataSource::Metadd_table_columnnullable=5-1UR::DataSource::Metadd_fk_constraintfk_constraint_name05'UR::DataSource::Metadd_tablelast_ddl_time<5;!UR::DataSource::Metadd_fk_constraint_columntable_name;5+/UR::DataSource::Metadd_bitmap_indexbitmap_index_name >5+5UR::DataSource::Metadd_table_columnlast_object_revision <5;!UR::DataSource::Metadd_pk_constraint_columntable_name A5C#UR::DataSource::Metadd_unique_constraint_columncolumn_name 55+#UR::DataSource::Metadd_table_columndata_length E5C+UR::DataSource::Metadd_unique_constraint_columnconstraint_name*5UR::DataSource::Metadd_tableer_type75;UR::DataSource::Metadd_fk_constraint_columnowner65;UR::DataSource::Metadd_pk_constraint_columnrank75;UR::DataSource::Metadd_pk_constraint_columnowner55+#UR::DataSource::Metadd_bitmap_i 55+#UR::DataSource::Metadd_table_columndata_length >5;%UR::DataSource::Metadd_fk_constraint_columnr_table_name =Q+indexsqlite_autoindex_dd_table_column_1dd_table_column ctabledd_tabledd_tableCREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) )/Cindexsqlite_autoindex_dd_table_1dd_table ++Qtabledd_bitmap_indexdd_bitmap_indexCREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) )=Q+indexsqlite_autoindex_dd_bitmap_index_1dd_bitmap_index 9x++ktabledd_table_columndd_table_columnCREATE TABLE dd_table_column ( data_sourc++ktabledd_table_columndd_table_columnCREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) )=Q+indexsqlite_autoindex_dd_table_column_1dd_table_column6;;tabledd_pk_constraint_columndd_pk_constraint_columnCREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) )Ma;indexsqlite_autoindex_dd_pk_constraint_column_1dd_pk_constraint_column 9SQK9Z 5;!3 UR::DataSource::Metadd_pk_constraint_columntable_namevarcharN2007-04-16 19:35:07_ 5C#3 UR::DataSource::Metadd_unique_constraint_columncolumn_namevarcharN2007-04-16 19:35:06S 5+#3 UR::DataSource::Metadd_table_columndata_lengthvarcharY2007-04-16 19:35:06c 5C+3 UR::DataSource::Metadd_unique_constraint_columnconstraint_namevarcharN2007-04-16 19:35:06H 53 UR::DataSource::Metadd_tableer_typevarcharN2007-04-16 19:35:07U 5;3 UR::DataSource::Metadd_fk_constraint_columnownervarcharN2007-04-16 19:35:07T 5;3 UR::DataSource::Metadd_pk_constraint_columnrankintegerN2007-04-16 19:35:07U 5;3 UR::DataSource::Metadd_pk_constraint_columnownervarcharY2007-04-16 19:35:07S 5+#3 UR::DataSource::Metadd_bitmap_indexdata_sourcevarcharN2007-04-16 19:35:06R 5+!3 UR::DataSource::Metadd_bitmap_indextable_namevarcharN2007-04-16 19:35:06W 553 UR::DataSource::Metadd_tablelast_object_revisiontimestampN2007-04-16 19:35:07 TE:?TF 53 UR::DataSource::Metadd_tableownervarcharY2007-04-16 19:35:07M 5+3 UR::DataSource::Metadd_bitmap_indexownervarcharY2007-04-16 19:35:06R 5+!3 UR::DataSource::Metadd_table_columntable_namevarcharN2007-04-16 19:35:06Q 5+3 UR::DataSource::Metadd_table_columndata_typevarcharN2007-04-16 19:35:06T 5-#3 UR::DataSource::Metadd_fk_constraintdata_sourcevarcharN2007-04-16 19:35:07P 5+3 UR::DataSource::Metadd_table_columnnullablevarcharN2007-04-16 19:35:06[ 5-13 UR::DataSource::Metadd_fk_constraintfk_constraint_namevarcharN2007-04-16 19:35:07P 5'3 UR::DataSource::Metadd_tablelast_ddl_timetimestampY2007-04-16 19:35:07Z 5;!3 UR::DataSource::Metadd_fk_constraint_columntable_namevarcharN2007-04-16 19:35:07Y 5+/3 UR::DataSource::Metadd_bitmap_indexbitmap_index_namevarcharN2007-04-16 19:35:06^ 5+53 UR::DataSource::Metadd_table_columnlast_object_revisiontimestampN2007-04-16 19:35:06 iQ+\iC25+U;5+/UR::DataSource::M=5;#UR::DataSour55-!UR::DataSource::Metadd_fk_constrainttable_name-?5-5UR::DataSource::Metadd_fk_constraintlast_object_revision,;5+/UR::DataSource::Metadd_bitmap_indexbitmap_index_name 55+#UR::DataSource::Metadd_bitmap_indexdata_source/5+UR::DataSource::Metadd_bitmap_indexowner45+!UR::DataSource::Metadd_bitmap_indextable_name65-#UR::DataSource::Metadd_fk_constraintdata_source=5-1UR::DataSource::Metadd_fk_constraintfk_constraint_name05-UR::DataSource::Metadd_fk_constraintowner)25-UR::DataSource::Metadd_fk_constraintr_owner"75-%UR::DataSource::Metadd_fk_constraintr_table_name=5;#UR::DataSource::Metadd_fk_constraint_columncolumn_name(=5;#UR::DataSource::Metadd_fk_constraint_columndata_sourceD5;1UR::DataSource::Metadd_fk_constraint_columnfk_constraint_name'75;UR::DataSource::Metadd_fk_constraint_columnowner?5;'UR::DataSource::Metadd_fk_constraint_columnr_column_name% /mWCn=05'UR::DataSource::Metadd_tablelast_ddl_time755UR::DataSource::Metadd_tablelast_object_revision(5UR::DataSource::Metadd_tableowner*5UR::DataSource::Metadd_tableremarks-<5;!UR::DataSource::Metadd_fk_constraint_columntable_name=5;#UR::DataSource::Metadd_pk_constraint_columncolumn_name&=5;#UR::DataSource::Metadd_pk_constraint_columndata_source75;UR::DataSource::Metadd_pk_constraint_columnowner65;UR::DataSource::Metadd_pk_constraint_columnrank<5;!UR::DataSource::Metadd_pk_constraint_columntable_name .5#UR::DataSource::Metadd_tabledata_source$*5UR::DataSource::Metadd_tableer_type05'UR::DataSource::Metadd_tablelast_ddl_time755UR::DataSource::Metadd_tablelast_object_revision(5UR::DataSource::Metadd_tableowner*5UR::DataSource::Metadd_tableremarks-5!UR::DataSource::Metadd_tabletable_name-5!UR::DataSource::Metadd_tabletable_type55+#UR::DataSource::Metadd_table_columncolumn_name WLIeWS! 5+#3 UR::DataSource::Metadd_table_columndata_sourcevarcharN2007-04-16 19:35:06Y 5C3 UR::DataSource::Metadd_unique_constraint_columnownervarcharY2007-04-16 19:35:06\ 5;%3 UR::DataSource::Metadd_fk_constraint_columnr_table_namevarcharN2007-04-16 19:35:07H 53 UR::DataSource::Metadd_tableremarksvarcharY2007-04-16 19:35:07K 5!3 UR::DataSource::Metadd_tabletable_typevarcharN2007-04-16 19:35:07K 5!3 UR::DataSource::Metadd_tabletable_namevarcharN2007-04-16 19:35:07S 5+#3 UR::DataSource::Metadd_table_columncolumn_namevarcharN2007-04-16 19:35:06[ 5;#3 UR::DataSource::Metadd_pk_constraint_columndata_sourcevarcharN2007-04-16 19:35:07O 5+3 UR::DataSource::Metadd_table_columnremarksvarcharY2007-04-16 19:35:06U 5-%3 UR::DataSource::Metadd_fk_constraintr_table_namevarcharN2007-04-16 19:35:07[ 5;#3 UR::DataSource::Metadd_fk_constraint_columndata_sourcevarcharN2007-04-16 19:35:07 "ND3"_, 5-53 UR::DataSource::Metadd_fk_constraintlast_object_revisiontimestampN2007-04-16 19:35:07M+ 5+3 UR::DataSource::Metadd_table_columnownervarcharY2007-04-16 19:35:06_* 5C#3 UR::DataSource::Metadd_unique_constraint_columndata_sourcevarcharN2007-04-16 19:35:06N) 5-3 UR::DataSource::Metadd_fk_constraintownervarcharY2007-04-16 19:35:07[( 5;#3 UR::DataSource::Metadd_fk_constraint_columncolumn_namevarcharN2007-04-16 19:35:07b' 5;13 UR::DataSource::Metadd_fk_constraint_columnfk_constraint_namevarcharN2007-04-16 19:35:07[& 5;#3 UR::DataSource::Metadd_pk_constraint_columncolumn_namevarcharN2007-04-16 19:35:07]% 5;'3 UR::DataSource::Metadd_fk_constraint_columnr_column_namevarcharN2007-04-16 19:35:07L$ 5#3 UR::DataSource::Metadd_tabledata_sourcevarcharN2007-04-16 19:35:07^# 5C!3 UR::DataSource::Metadd_unique_constraint_columntable_namevarcharN2007-04-16 19:35:06P" 5-3 UR::DataSource::Metadd_fk_constraintr_ownervarcharY2007-04-16 19:35:07 F"aF=v/5+UR::DataSource::Metadd_table_columnowner+A5C#UR::DataSource::Metadd_unique_constraint_columndata_source*55+#UR::DataSource::Metadd_table_columndata_source!35+UR::DataSource::Metadd_table_columndata_type>5+5UR::DataSource::Metadd_table_columnlast_object_revision 25+UR::DataSource::Metadd_table_columnnullable15+UR::DataSource::Metadd_table_columnremarks45+!UR::DataSource::Metadd_table_columntable_nameA5C#UR::DataSource::Metadd_unique_constraint_columncolumn_name E5C+UR::DataSource::Metadd_unique_constraint_columnconstraint_name;5CUR::DataSource::Metadd_unique_constraint_columnowner @5C!UR::DataSource::Metadd_unique_constraint_columntable_name# S- 5-!3 UR::DataSource::Metadd_fk_constrainttable_namevarcharN2007-04-16 19:35:07f0A o,A5C#UR::DataSource::Metadd_unique_constraint_columndata_source/5+UR::DataSource::Metadd_bitmap_indexowner05-UR::DataSource::Metadd_fk_constraintowner65-#UR::DataSource::Metadd_fk_constraintdata_source5 5+#UR::DataSource::Metadd_table_columncolumn_name= 5;#UR::DataSource::Metadd_pk_constraint_columndata_source7 5;UR::DataSource::Metadd_fk_constraint_columnowner6 5;UR::DataSource::Metadd_pk_constraint_columnrank= 5;#UR::DataSource::Metadd_fk_constraint_columncolumn_name45+!UR::DataSource::Metadd_bitmap_indextable_name25-UR::DataSource::Metadd_fk_constraintr_owner<5;!UR::DataSource::Metadd_pk_constraint_columntable_name55-!UR::DataSource::Metadd_fk_constrainttable_name75;UR::DataSource::Metadd_pk_constraint_columnowner45+!UR::DataSource::Metadd_table_columntable_name=5;#UR::DataSource::Metadd_fk_constraint_columndata_sourceA5C#UR::DataSource::Metadd_unique_constraint_columncolumn__|u5 E15+UR::DataSource::Metadd_bitmap_indexowner25-UR::DataSource::Metadd_fk_constraintowner85-#UR::DataSource::Metadd_fk_constraintdata_source75+#UR::DataSource::Metadd_table_columncolumn_name ?5;#UR::DataSource::Metadd_pk_constraint_columndata_source 95;UR::DataSource::Metadd_fk_constraint_columnowner 85;UR::DataSource::Metadd_pk_constraint_columnrank ?5;#UR::DataSource::Metadd_fk_constraint_columncolumn_name 65+!UR::DataSource::Metadd_bitmap_indextable_name45-UR::DataSource::Metadd_fk_constraintr_owner>5;!UR::DataSource::Metadd_pk_constraint_columntable_name75-!UR::DataSource::Metadd_fk_constrainttable_name95;UR::DataSource::Metadd_pk_constraint_columnowner65+!UR::DataSource::Metadd_table_columntable_name?5;#UR::DataSource::Metadd_fk_constraint_columndata_source?5;#UR::DataSource::Metadd_pk_constraint_columndata_source N6mF}N=>5;!UR::DataSource::Metadd_fk_constraint_columntable_name 95-%UR::DataSource::Metadd_fk_constraintr_table_name=5+/UR::DataSource::Metadd_bitmap_indexbitmap_index_name75+#UR::DataSource::Metadd_bitmap_indexdata_source?5;#UR::DataSource::Metadd_pk_constraint_columncolumn_nameF5;1UR::DataSource::Metadd_fk_constraint_columnfk_constraint_name?5-1UR::DataSource::Metadd_fk_constraintfk_constraint_name15+UR::DataSource::Metadd_bitmap_indexowner65+!UR::DataSource::Metadd_bitmap_indextable_name85-#UR::DataSource::Metadd_fk_constraintdata_source25-UR::DataSource::Metadd_fk_constraintowner45-UR::DataSource::Metadd_fk_constraintr_owner75-!UR::DataSource::Metadd_fk_constrainttable_name?5;#UR::DataSource::Metadd_fk_constraint_columncolumn_name ?5;#UR::DataSource::Metadd_fk_constraint_columndata_source95;UR::DataSource::Metadd_fk_constraint_columnowner W+ \Ax*5UR::DataSource::Metadd_tableowner75+#UR::DataSource::Metadd_table_columndata_source/5!UR::DataSource::Metadd_tabletable_name05#UR::DataSource::Metadd_tabledata_source15+UR::DataSource::Metadd_table_columnowner=5CUR::DataSource::Metadd_unique_constraint_columnownerB5C!UR::DataSource::Metadd_unique_constraint_columntable_nameG5C+UR::DataSource::Metadd_unique_constraint_columnconstraint_name95;UR::DataSource::Metadd_pk_constraint_columnowner85;UR::DataSource::Metadd_pk_constraint_columnrank >5;!UR::DataSource::Metadd_pk_constraint_columntable_name75+#UR::DataSource::Metadd_table_columncolumn_name 65+!UR::DataSource::Metadd_table_columntable_nameC5C#UR::DataSource::Metadd_unique_constraint_columncolumn_nameC5C#UR::DataSource::Metadd_unique_constraint_columndata_source ,~Hf0A o,A5C#UR::DataSource::Metadd_unique_constraint_columndata_source/5+UR::DataSource::Metadd_bitmap_indexowner05-UR::DataSource::Metadd_fk_constraintowner65-#UR::DataSource::Metadd_fk_constraintdata_source5 5+#UR::DataSource::Metadd_table_columncolumn_name= 5;#UR::DataSource::Metadd_pk_constraint_columndata_source7 5;UR::DataSource::Metadd_fk_constraint_columnowner6 5;UR::DataSource::Metadd_pk_constraint_columnrank= 5;#UR::DataSource::Metadd_fk_constraint_columncolumn_name45+!UR::DataSource::Metadd_bitmap_indextable_name25-UR::DataSource::Metadd_fk_constraintr_owner<5;!UR::DataSource::Metadd_pk_constraint_columntable_name55-!UR::DataSource::Metadd_fk_constrainttable_name75;UR::DataSource::Metadd_pk_constraint_columnowner45+!UR::DataSource::Metadd_table_columntable_name=5;#UR::DataSource::Metadd_fk_constraint_columndata_sourceA5C#UR::DataSource::Metadd_unique_constraint_columncolumn_name z4vEr;< 5;!UR::DataSource::Metadd_fk_constraint_columntable_name75-%UR::DataSource::Metadd_fk_constraintr_table_name(5UR::DataSource::Metadd_tableowner55+#UR::DataSource::Metadd_table_columndata_source-5!UR::DataSource::Metadd_tabletable_name;5+/UR::DataSource::Metadd_bitmap_indexbitmap_index_name.5#UR::DataSource::Metadd_tabledata_source55+#UR::DataSource::Metadd_bitmap_indexdata_source/5+UR::DataSource::Metadd_table_columnowner=5;#UR::DataSource::Metadd_pk_constraint_columncolumn_name;5CUR::DataSource::Metadd_unique_constraint_columnowner@5C!UR::DataSource::Metadd_unique_constraint_columntable_nameD5;1UR::DataSource::Metadd_fk_constraint_columnfk_constraint_name=5-1UR::DataSource::Metadd_fk_constraintfk_constraint_nameE5C+UR::DataSource::Metadd_unique_constraint_columnconstraint_name   plX CC7tabledd_unique_constraint_columndd_unique_constraint_columnCREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, X CC7tabledd_unique_constraint_columndd_unique_constraint_columnCREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) )U iCindexsqlite_autoindex_dd_unique_constraint_column_1dd_unique_constraint_column --Mtabledd_fk_constraintdd_fk_constraintCREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) )     ? S-indexsqlite_autoindex_dd_fk_constraint_1dd_fk_constraint, ;;otabledd_fk_constraint_columndd_fk_constraint_columnCREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar NOT NULL, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) )Ma;indexsqlite_autoindex_dd_fk_constraint_column_1dd_fk_constraint_columnRDBMS.pm000444023532023421 36763112121654173 16233 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::RDBMS; # NOTE:: UR::DataSource::QueryPlan has conditional logic # for this class/subclasses currently use strict; use warnings; use Scalar::Util; use File::Basename; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS', is => ['UR::DataSource','UR::Singleton'], is_abstract => 1, properties => [ server => { is => 'String', doc => 'the "server" part of the DBI connect string' }, login => { is => 'String', doc => 'user name to connect as', is_optional => 1 }, auth => { is => 'String', doc => 'authentication for the given user', is_optional => 1 }, owner => { is => 'String', doc => 'Schema/owner name to connect to', is_optional => 1 }, ], has_optional => [ _all_dbh_hashref => { type => 'HASH', len => undef, is_transient => 1 }, _default_dbh => { type => 'DBI::db', len => undef, is_transient => 1 }, _last_savepoint => { type => 'String', len => undef, is_transient => 1 }, ], valid_signals => ['query'], doc => 'A logical DBI-based database, independent of prod/dev/testing considerations or login details.', ); sub database_exists { my $self = shift; warn $self->class . " failed to implement the database_exists() method. Testing connection as a surrogate. FIXME here!\n"; eval { my $c = $self->create_dbh(); }; if ($@) { return; } return 1; } sub create_database { my $self = shift; die $self->class . " failed to implement the create_database() method!" . " Unable to initialize a new database for this data source " . $self->__display_name__ . " FIXME here.\n"; } sub _resolve_ddl_for_table { my ($self,$table, %opts) = @_; my $all = delete $opts{all}; if (%opts) { Carp::confess("odd arguments to _resolve_ddl_for_table: " . UR::Util::d(\%opts)); } my $table_name = $table->table_name; my @ddl; if ($table->{db_committed} and not $all) { my @columns = $table->columns; for my $column (@columns) { next unless $all or $column->last_object_revision eq '-'; my $column_name = $column->column_name; my $ddl = "alter table $table_name add column "; $ddl .= "\t$column_name " . $column->data_type; if ($column->data_length) { $ddl .= '(' . $column->data_length . ')'; } push(@ddl, $ddl) if $ddl; } } else { my $ddl; my @columns = $table->columns; for my $column (@columns) { next unless $all or $column->last_object_revision eq '-'; my $column_name = $column->column_name; $ddl = 'create table ' . $table_name . "(\n" unless defined $ddl; $ddl .= "\t$column_name " . $column->data_type; if ($column->data_length) { $ddl .= '(' . $column->data_length . ')'; } $ddl .= ",\n" unless $column eq $columns[-1]; } $ddl .= "\n)" if defined $ddl; push(@ddl, $ddl) if $ddl; } return @ddl; } sub generate_schema_for_class_meta { my ($self,$class_meta,$temp) = @_; # We now support on-the-fly database introspection # this gets called with the temp flag when _sync_database realizes # it knows nothing about the table in question. # We basically presume the schema is the one we would have generated # given the current class definitions # TODO: We still need to presume foreign keys are constrained. my $method = ($temp ? '__define__' : 'create'); my @defined; my $table_name = $class_meta->table_name; my @fks_to_generate; for my $p ($class_meta->parent_class_metas) { next if ($p->class_name eq 'UR::Object' or $p->class_name eq 'UR::Entity'); next unless $p->class_name->isa("UR::Object"); my @new = $self->generate_schema_for_class_meta($p,$temp); push @defined, @new; my $parent_table; if (($parent_table) = grep { $_->isa("UR::DataSource::RDBMS::Table") } @new) { my @id_by = $class_meta->id_property_names; my @column_names = map { $class_meta->property($_)->column_name } @id_by; my $r_table_name = $parent_table->table_name; ##$DB::single = 1; # get pk columns my @r_id_by = $p->id_property_names; my @r_column_names = map { $class_meta->property($_)->column_name } @r_id_by; push @fks_to_generate, [$class_meta->class_name, $table_name, $r_table_name, \@column_names, \@r_column_names]; } } my %properties_with_expected_columns = map { $_->column_name => $_ } grep { $_->column_name } $class_meta->direct_property_metas; #my %expected_constraints = # map { $_->column_name => $_ } # grep { $_->class_meta eq $class_meta } # map { $class_meta->property_meta_for_name($_) } # map { @{ $_->id_by } } # grep { $_->id_by } # $class_meta->all_property_metas; #print Data::Dumper::Dumper(\%expected_constraints); unless ($table_name) { if (my @column_names = keys %properties_with_expected_columns) { Carp::confess("class " . $class_meta->__display_name__ . " has no table_name specified for columns @column_names!"); } else { # no table, but no storable columns. all ok. return; } } ## print "handling table $table_name\n"; if ($table_name =~ /[^\w\.]/) { # pass back anything from parent classes, but do nothing for special "view" tables #$DB::single = 1; return @defined; } my $t = '-'; my $table = $self->refresh_database_metadata_for_table_name($table_name, $method); my %existing_columns; if ($table) { ## print "found table $table_name\n"; %existing_columns = map { $_->column_name => $_ } grep { $_->column_name } $table->columns; push @defined, ($table,$table->columns); } else { ## print "adding table $table_name\n"; my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); $table = UR::DataSource::RDBMS::Table->$method( table_name => $ds_table, data_source => $self->_my_data_source_id, owner => $ds_owner, remarks => $class_meta->doc, er_type => 'entity', last_object_revision => $t, table_type => ($table_name =~ /\s/ ? 'view' : 'table'), ); Carp::confess("Failed to create metadata or table $table_name") unless $table; push @defined, $table; } my ($update,$add,$extra) = UR::Util::intersect_lists([keys %properties_with_expected_columns],[keys %existing_columns]); for my $column_name (@$extra) { my $column = $existing_columns{$column_name}; $column->last_object_revision('?'); } for my $column_name (@$add) { my $property = $properties_with_expected_columns{$column_name}; #print "adding column $column_name\n"; my $column = UR::DataSource::RDBMS::TableColumn->$method( column_name => $column_name, table_name => $table->table_name, data_source => $table->data_source, namespace => $table->namespace, owner => $table->owner, data_type => $self->object_to_db_type($property->data_type) || 'Text', data_length => $property->data_length, nullable => $property->is_optional, remarks => $property->doc, last_object_revision => $t, ); push @defined, $column; } for my $column_name (@$update) { my $property = $properties_with_expected_columns{$column_name}; my $column = $existing_columns{$column_name}; ##print "updating column $column_name with data from property " . $property->property_name . "\n"; if ($column->data_type) { $column->data_type($self->object_to_db_type($property->data_type)) if $property->data_type; } else { $column->data_type($self->object_to_db_type($property->data_type) || 'Text'); } $column->data_length($property->data_length); $column->nullable($property->is_optional); $column->remarks($property->doc); } for my $property ( $class_meta->direct_id_property_metas ) { unless (UR::DataSource::RDBMS::PkConstraintColumn->get(table_name => $table->table_name, owner => $table->owner, column_name => $property->column_name, data_source => $table->data_source)) { UR::DataSource::RDBMS::PkConstraintColumn->$method( column_name => $property->column_name, data_source => $table->data_source, owner => $table->owner, rank => $property->is_id, table_name => $table->table_name ); } } # this "property_metas" method filers out things which have an id_by. # it used to call ->properties, which used that method internally ...but seems like it never could have done anything? for my $property ($class_meta->property_metas) { my $id_by = $property->id_by; next unless $id_by; my $r_class_name = $property->data_type; my $r_class_meta = $r_class_name->__meta__; my $r_table_name = $r_class_meta->table_name; next unless $r_table_name; my @column_names = map { $class_meta->property($_)->column_name } @$id_by; my @r_column_names = map { $r_class_meta->property($_)->column_name } @{ $r_class_meta->id_property_names }; push @fks_to_generate, [$property->id, $table_name, $r_table_name, \@column_names, \@r_column_names ]; } for my $fk_to_generate (@fks_to_generate) { my ($fk_id, $table_name, $r_table_name, $column_names, $r_column_names) = @$fk_to_generate; my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my($ds_r_owner, $ds_r_table) = $self->_resolve_owner_and_table_from_table_name($r_table_name); my $fk = UR::DataSource::RDBMS::FkConstraint->$method( fk_constraint_name => $fk_id, table_name => $ds_table, r_table_name => $ds_r_table, owner => $ds_owner, r_owner => $ds_owner, data_source => $self->_my_data_source_id, last_object_revision => '-', ); unless ($fk) { die "failed to generate an implied foreign key constraint for $table_name => $r_table_name!" . UR::DataSource::RDBMS::FkConstraint->error_message; } push @defined, $fk; for (my $n = 0; $n < @$column_names; $n++) { my $column_name = $column_names->[$n]; my $r_column_name = $r_column_names->[$n]; my %fkcol_params = ( fk_constraint_name => $fk_id, table_name => $ds_table, column_name => $column_name, r_table_name => $ds_r_table, r_column_name => $r_column_name, owner => $ds_owner, data_source => $self->_my_data_source_id, ); my $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params); unless ($fkcol) { $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->$method(%fkcol_params); } unless ($fkcol) { die "failed to generate an implied foreign key constraint for $table_name => $r_table_name!" . UR::DataSource::RDBMS::FkConstraint->error_message; } push @defined, $fkcol; } } # handle missing meta datasource on the fly... if (@defined) { my $ns = $class_meta->namespace; my $exists = UR::Object::Type->get($ns . "::DataSource::Meta"); unless ($exists) { UR::DataSource::Meta->generate_for_namespace($ns); } } unless ($temp) { my @ddl = $self->_resolve_ddl_for_table($table); $t = $UR::Context::current->now; if (@ddl) { my $dbh = $table->data_source->get_default_handle; for my $ddl (@ddl) { $dbh->do($ddl) or Carp::confess("Failed to modify the database schema!: $ddl\n" . $dbh->errstr); for my $o ($table, $table->columns) { $o->last_object_revision($t); } } } } return @defined; } # override in architecture-oriented subclasses sub object_to_db_type { my ($self, $object_type) = @_; my $db_type = $object_type; # ... return $db_type; } # override in architecture-oriented subclasses sub db_to_object_type { my ($self, $db_type) = @_; my $object_type = $db_type; # ... return $object_type; } # FIXME - shouldn't this be a property of the class instead of a method? sub does_support_joins { 1 } sub get_class_meta_for_table { my $self = shift; my $table = shift; my $table_name = $table->table_name; return $self->get_class_meta_for_table_name($table_name); } sub get_class_meta_for_table_name { my($self,$table_name) = @_; # There is an unique constraint on classes, but only those which use # tables in an RDBMS, which dicates that there can be only two for # a given table in a given data source: one for the ghost and one # for the regular entity. We can't just fix this with a unique constraint # since classes with a null data source would be lost in some queries. my @class_meta = grep { not $_->class_name->isa("UR::Object::Ghost") } UR::Object::Type->get( table_name => $table_name, data_source => $self->class, ); unless (@class_meta) { # This will load every class in the namespace on the first execution :( ##$DB::single = 1; @class_meta = grep { not $_->class_name->isa("UR::Object::Ghost") } UR::Object::Type->get( table_name => $table_name, data_source => $self->class, ); } $self->context_return(@class_meta); } sub dbi_data_source_name { my $self = shift->_singleton_object; my $driver = $self->driver; my $server = $self->server; unless ($driver) { Carp::confess("Cannot resolve a dbi_data_source_name with an undefined driver()"); } unless ($server) { Carp::confess("Cannot resolve a dbi_data_source_name with an undefined server()"); } return 'dbi:' . $driver . ':' . $server; } sub get_default_handle { my $self = shift->_singleton_object; my $dbh = $self->_default_dbh; unless ($dbh && $dbh->{Active}) { $dbh = $self->create_dbh(); $self->_default_dbh($dbh); } return $dbh; } *get_default_dbh = \&get_default_handle; *has_default_dbh = \&has_default_handle; *disconnect_default_dbh = \&disconnect_default_handle; sub has_default_handle { my $self = shift->_singleton_object; return 1 if $self->_default_dbh; return; } sub disconnect_default_handle { my $self = shift->_singleton_object; my $dbh = $self->_default_dbh; unless ($dbh) { Carp::cluck("Cannot disconnect. Not connected!"); return; } $dbh->disconnect; $self->_default_dbh(undef); return $dbh; } sub get_for_dbh { my $class = shift; my $dbh = shift; my $ds_name = $dbh->{"private_UR::DataSource::RDBMS_name"}; return unless($ds_name); my $ds = UR::DataSource->get($ds_name); return $ds; } sub has_changes_in_base_context { shift->has_default_dbh; # TODO: actually check, as this is fairly conservative # If used for switching contexts, we'd need to safely rollback any transactions first. } sub _dbi_connect_args { my $self = shift; my @connection; $connection[0] = $self->dbi_data_source_name; $connection[1] = $self->login; $connection[2] = $self->auth; $connection[3] = { AutoCommit => 0, RaiseError => 0 }; return @connection; } sub get_connection_debug_info { my $self = shift; my @debug_info = ( "DBI Data Source Name: ", $self->dbi_data_source_name, "\n", "DBI Login: ", $self->login, "\n", "DBI Version: ", $DBI::VERSION, "\n", "DBI Error: ", UR::DBI->errstr, "\n", ); return @debug_info; } sub create_dbh { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } # get connection information my @connection = $self->_dbi_connect_args(); # connect my $dbh = UR::DBI->connect(@connection); unless ($dbh) { my @confession = ( "Failed to connect to the database!\n", $self->get_connection_debug_info(), ); Carp::confess(@confession); } # used for reverse lookups $dbh->{'private_UR::DataSource::RDBMS_name'} = $self->class; # this method may be implemented in subclasses to do extra initialization if ($self->can("_init_created_dbh")) { unless ($self->_init_created_dbh($dbh)) { $dbh->disconnect; Carp::confess("Failed to initialize new database connection!\n" . $self->error_message . "\n"); } } # store the handle in a hash, since it's not a UR::Object my $all_dbh_hashref = $self->_all_dbh_hashref; unless ($all_dbh_hashref) { $all_dbh_hashref = {}; $self->_all_dbh_hashref($all_dbh_hashref); } $all_dbh_hashref->{$dbh} = $dbh; Scalar::Util::weaken($all_dbh_hashref->{$dbh}); $self->is_connected(1); return $dbh; } sub _init_created_dbh { # override in sub-classes 1; } # The default is to ignore no tables, but derived classes # will probably override this sub _ignore_table { 0; } sub _get_table_names_from_data_dictionary { my $self = shift->_singleton_object; if (@_) { Carp::confess("get_tables does not currently take filters! FIXME."); } my $dbh = $self->get_default_handle; my $owner = $self->owner; # FIXME This will fix the immediate problem of getting classes to be created out of # views. We still need to somehow mark the resulting class as read-only my $sth = $dbh->table_info("%", $owner, "%", "TABLE,VIEW"); my $table_name; $sth->bind_col(3,\$table_name); my @names; while ($sth->fetch) { next if $self->_ignore_table($table_name); $table_name =~ s/"|'//g; # Postgres puts quotes around entities that look like keywords push @names, $table_name; } return @names; } # A wrapper for DBI's table_info() since the DBD implementations of them # aren't always exactly what we need in other places in the system. Other # subclasses can override it to get custom behavior sub get_table_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('table_info',@_); } sub _get_whatever_details_from_data_dictionary { my $self = shift; my $method = shift; my $dbh = $self->get_default_handle(); return unless $dbh; return $dbh->$method(@_); } sub get_column_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('column_info',@_); } sub get_foreign_key_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('foreign_key_info',@_); } sub get_primary_key_details_from_data_dictionary { return shift->_get_whatever_details_from_data_dictionary('primary_key_info',@_); } sub get_table_names { map { $_->table_name } shift->get_tables(@_); } sub get_tables { my $self = shift; #my $class = shift->_singleton_class_name; #return UR::DataSource::RDBMS::Table->get(data_source_id => $class); my $ds_id; if (ref $self) { if ($self->can('id')) { $ds_id = $self->id; } else { $ds_id = ref $self; } } else { $ds_id = $self; } return UR::DataSource::RDBMS::Table->get(data_source => $ds_id); } sub get_nullable_foreign_key_columns_for_table { my $self = shift; my $table = shift; my @nullable_fk_columns; my @fk = $table->fk_constraints; for my $fk (@fk){ my @fk_columns = UR::DataSource::RDBMS::FkConstraintColumn->get( fk_constraint_name => $fk->fk_constraint_name, owner => $table->owner, data_source => $self->_my_data_source_id); for my $fk_col (@fk_columns){ my $column_obj = UR::DataSource::RDBMS::TableColumn->get(data_source => $self->_my_data_source_id, table_name => $fk_col->table_name, owner => $fk_col->owner, column_name=> $fk_col->column_name); unless ($column_obj) { Carp::croak("Can't find TableColumn metadata object for table name ".$fk_col->table_name." column ".$fk_col->column_name." while processing foreign key constraint named ".$fk->fk_constraint_name); } if ($column_obj->nullable and $column_obj->nullable ne 'N'){ my $col = $column_obj->column_name; push @nullable_fk_columns, $col; } } } return @nullable_fk_columns; } sub get_non_primary_key_nullable_foreign_key_columns_for_table { my $self = shift; my $table = shift; my @nullable_fk_columns = $self->get_nullable_foreign_key_columns_for_table($table); my %pk_columns = map { $_->column_name => 1} $table->primary_key_constraint_columns; my @non_pk_nullable_fk_columns; for my $fk_column (@nullable_fk_columns){ push @non_pk_nullable_fk_columns, $fk_column unless grep { $fk_column eq $_} keys %pk_columns; } return @non_pk_nullable_fk_columns; } # TODO: make "env" an optional characteristic of a class attribute # for all of the places we do this crap... sub access_level { my $self = shift; my $env = $self->_method2env("access_level"); if (@_) { if ($self->has_default_dbh) { Carp::confess("Cannot change the db access level for $self while connected!"); } $ENV{$env} = lc(shift); } else { $ENV{$env} ||= "ro"; } return $ENV{$env}; } sub _method2env { my $class = shift; my $method = shift; unless ($method =~ /^(.*)::([^\:]+)$/) { $class = ref($class) if ref($class); $method = $class . "::" . $method; } $method =~ s/::/__/g; return $method; } sub resolve_class_name_for_table_name { my $self = shift->_singleton_class_name; my $table_name = shift; my $relation_type = shift; # Should be 'TABLE' or 'VIEW' # When a table_name conflicts with a reserved word, it ends in an underscore. $table_name =~ s/_$//; my $namespace = $self->get_namespace; my $vocabulary = $namespace->get_vocabulary; my @words; $vocabulary = 'UR::Vocabulary' unless eval { $vocabulary->__meta__ }; if ($vocabulary) { @words = map { $vocabulary->convert_to_title_case($_) } map { $vocabulary->plural_to_singular($_) } map { lc($_) } split("_",$table_name); } else { @words = map { ucfirst(lc($_)) } split("_",$table_name); } if ($self->can('_resolve_class_name_for_table_name_fixups')) { @words = $self->_resolve_class_name_for_table_name_fixups(@words); } my $class_name; my $addl; if ($relation_type && $relation_type =~ m/view/i) { $addl = 'View::'; } else { # Should just be for tables, temp tables, etc $addl = ''; } $class_name = $namespace . "::" . $addl . join("",@words); if (substr($class_name, -6) eq '::Type') { # Don't overwrite class metadata objects for a table called 'type' $class_name .= 'Table'; $self->warning_message("Class for table $table_name will be $class_name"); } return $class_name; } sub resolve_type_name_for_table_name { my $self = shift->_singleton_class_name; my $table_name = shift; my $namespace = $self->get_namespace; my $vocabulary = $namespace->get_vocabulary; $vocabulary = 'UR::Vocabulary' unless eval { $vocabulary->__meta__ }; my $vocab_obj = eval { $vocabulary->__meta__ }; my @words = ( ( map { $vocabulary->plural_to_singular($_) } map { lc($_) } split("_",$table_name) ) ); my $type_name = join(" ",@words); return $type_name; } sub resolve_property_name_for_column_name { my $self = shift->_singleton_class_name; my $column_name = shift; my @words = map { lc($_) } split("_",$column_name); my $type_name = join("_",@words); return $type_name; } sub _get_or_create_table_meta { my $self = shift; my ($data_source, $ur_owner, $ur_table_name, $db_table_name, $creation_method, $table_data, $revision_time) = @_; my $data_source_id = $self->_my_data_source_id; my $table_object = UR::DataSource::RDBMS::Table->get(data_source => $data_source_id, owner => $ur_owner, table_name => $ur_table_name); if ($table_object) { # Already exists, update the existing entry # Instead of deleting and recreating the table object (the old way), # modify its attributes in-place. The name can't change but all the other # stuff might. $table_object->table_type($table_data->{TABLE_TYPE}); $table_object->owner($table_data->{TABLE_SCHEM}); $table_object->data_source($data_source->class); $table_object->remarks($table_data->{REMARKS}); $table_object->last_object_revision($revision_time) if ($table_object->__changes__()); } else { # Create a brand new one from scratch $table_object = UR::DataSource::RDBMS::Table->$creation_method( table_name => $ur_table_name, table_type => $table_data->{TABLE_TYPE}, owner => $table_data->{TABLE_SCHEM}, data_source => $data_source_id, remarks => $table_data->{REMARKS}, last_object_revision => $revision_time, ); unless ($table_object) { Carp::confess("Failed to $creation_method table object for $db_table_name"); } } return $table_object; } sub refresh_database_metadata_for_table_name { my ($self,$db_table_name, $creation_method) = @_; $creation_method ||= 'create'; my $data_source = $self; my $ur_table_name = $db_table_name; my @column_objects; my @all_constraints; # this must be on or before the actual data dictionary queries my $revision_time = $UR::Context::current->now(); # We'll count a table object as changed even if any of the columns, # FKs, etc # were changed my $data_was_changed_for_this_table = 0; # The class definition can specify a table name as . to override the # data source's default schema/owner. my $ds_owner; ($ds_owner,$db_table_name) = $self->_resolve_owner_and_table_from_table_name($db_table_name); my $ur_owner; ($ur_owner, $ur_table_name) = $self->_resolve_owner_and_table_from_table_name($ur_table_name); #my $dd_table_name = $table_name; #if ($table_name =~ m/(\w+)\.(\w+)/) { # $ds_owner = $1; # $dd_table_name = $2; #} # TABLE my $table_sth = $data_source->get_table_details_from_data_dictionary('%', $ds_owner, $db_table_name, "TABLE,VIEW"); my $table_data = $table_sth->fetchrow_hashref(); unless ($table_data && %$table_data) { #$self->error_message("No data for table $table_name in data source $data_source."); return; } my $data_source_id = $data_source->_my_data_source_id; my $table_object = $self->_get_or_create_table_meta( $data_source, $ur_owner, $ur_table_name, $db_table_name, $creation_method, $table_data, $revision_time); # COLUMNS # mysql databases seem to require you to actually put in the database name in the first arg my $db_name = ($data_source->can('db_name')) ? $data_source->db_name : '%'; my $column_sth = $data_source->get_column_details_from_data_dictionary($db_name, $ds_owner, $db_table_name, '%'); unless ($column_sth) { $self->error_message("Error getting column data for table $db_table_name in data source $data_source."); return; } my $all_column_data = $column_sth->fetchall_arrayref({}); unless (@$all_column_data) { $self->error_message("No column data for table $db_table_name in data source $data_source_id"); return; } my %columns_to_delete = map {$_->column_name, $_} UR::DataSource::RDBMS::TableColumn->get( table_name => $ur_table_name, owner => $ur_owner, data_source => $data_source_id); for my $column_data (@$all_column_data) { #my $id = $table_name . '.' . $column_data->{COLUMN_NAME} $column_data->{'COLUMN_NAME'} =~ s/"|'//g; # Postgres puts quotes around things that look like keywords delete $columns_to_delete{$column_data->{'COLUMN_NAME'}}; my $column_obj = UR::DataSource::RDBMS::TableColumn->get(table_name => $ur_table_name, owner => $ur_owner, data_source => $data_source_id, column_name => $column_data->{'COLUMN_NAME'}); if ($column_obj) { # Already exists, change the attributes $column_obj->owner($table_object->{owner}); $column_obj->data_source($table_object->{data_source}); $column_obj->data_type($column_data->{TYPE_NAME}); $column_obj->nullable(substr($column_data->{IS_NULLABLE}, 0, 1)); $column_obj->data_length($column_data->{COLUMN_SIZE}); $column_obj->remarks($column_data->{REMARKS}); if ($column_obj->__changes__()) { $column_obj->last_object_revision($revision_time); $data_was_changed_for_this_table = 1; } } else { # It's new, create it from scratch $column_obj = UR::DataSource::RDBMS::TableColumn->$creation_method( column_name => $column_data->{COLUMN_NAME}, table_name => $ur_table_name, owner => $table_object->{owner}, data_source => $table_object->{data_source}, data_type => $column_data->{TYPE_NAME}, nullable => substr($column_data->{IS_NULLABLE}, 0, 1), data_length => $column_data->{COLUMN_SIZE}, remarks => $column_data->{REMARKS}, last_object_revision => $revision_time, ); $data_was_changed_for_this_table = 1; } unless ($column_obj) { Carp::confess("Failed to create a column ".$column_data->{'COLUMN_NAME'}." for table $db_table_name"); } push @column_objects, $column_obj; } for my $to_delete (values %columns_to_delete) { #$self->status_message("Detected column " . $to_delete->column_name . " has gone away."); $to_delete->delete; $data_was_changed_for_this_table = 1; } my $bitmap_data = $data_source->get_bitmap_index_details_from_data_dictionary($db_table_name); for my $index (@$bitmap_data) { #push @{ $embed{bitmap_index_names}{$table_object} }, $index->{'index_name'}; my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($index->{'table_name'}); my $column_object = UR::DataSource::RDBMS::TableColumn->is_loaded( table_name => $ds_table, owner => $ds_owner, data_source => $data_source_id, column_name => $index->{'column_name'}, ); } # Make a note of what FKs exist in the Meta DB involving this table my @fks_in_meta_db = UR::DataSource::RDBMS::FkConstraint->get(data_source => $data_source_id, owner => $ur_owner, table_name => $ur_table_name); push @fks_in_meta_db, UR::DataSource::RDBMS::FkConstraint->get(data_source => $data_source_id, r_owner => $ur_owner, r_table_name => $ur_table_name); my %fks_in_meta_db_by_fingerprint; foreach my $fk ( @fks_in_meta_db ) { my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_meta_db_by_fingerprint{$fingerprint} = $fk; } # constraints on this table against columns in other tables #my $db_owner = $data_source->owner; my $fk_sth = $data_source->get_foreign_key_details_from_data_dictionary('', $ds_owner, $db_table_name, '', '', ''); my %fk; # hold the fk constraints that this invocation of foreign_key_info created my @constraints; my %fks_in_real_db; if ($fk_sth) { while (my $data = $fk_sth->fetchrow_hashref()) { foreach ( qw( FK_NAME FK_TABLE_NAME FKTABLE_NAME UK_TABLE_NAME PKTABLE_NAME FK_COLUMN_NAME FKCOLUMN_NAME UK_COLUMN_NAME PKCOLUMN_NAME ) ) { next unless defined($data->{$_}); # Postgres puts quotes around things that look like keywords $data->{$_} =~ s/"|'//g; } my $constraint_name = $data->{'FK_NAME'}; my $fk_table_name = $data->{'FK_TABLE_NAME'} || $data->{'FKTABLE_NAME'}; my $r_table_name = $data->{'UK_TABLE_NAME'} || $data->{'PKTABLE_NAME'}; my $fk_column_name = $data->{'FK_COLUMN_NAME'} || $data->{'FKCOLUMN_NAME'}; my $r_column_name = $data->{'UK_COLUMN_NAME'} || $data->{'PKCOLUMN_NAME'}; # MySQL returns primary key info with foreign_key_info()!? # They show up here with no $r_table_name or $r_column_name next unless ($r_table_name and $r_column_name); my $fk = UR::DataSource::RDBMS::FkConstraint->get(fk_constraint_name => $constraint_name, table_name => $fk_table_name, owner => $table_object->{owner}, data_source => $data_source_id, r_table_name => $r_table_name ); unless ($fk) { $fk = UR::DataSource::RDBMS::FkConstraint->$creation_method( fk_constraint_name => $constraint_name, table_name => $fk_table_name, r_table_name => $r_table_name, owner => $table_object->{owner}, r_owner => $table_object->{owner}, data_source => $table_object->{data_source}, last_object_revision => $revision_time, ); $fk{$fk->id} = $fk; $data_was_changed_for_this_table = 1; } if ($fk{$fk->id}) { my %fkcol_params = ( fk_constraint_name => $constraint_name, table_name => $fk_table_name, column_name => $fk_column_name, r_table_name => $r_table_name, r_column_name => $r_column_name, owner => $table_object->{owner}, data_source => $table_object->{data_source}, ); my $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params); unless ($fkcol) { $fkcol = UR::DataSource::RDBMS::FkConstraintColumn->$creation_method(%fkcol_params); } } my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_real_db{$fingerprint} = $fk; push @constraints, $fk; } } # get foreign_key_info the other way # constraints on other tables against columns in this table my $fk_reverse_sth = $data_source->get_foreign_key_details_from_data_dictionary('', '', '', '', $ds_owner, $db_table_name); %fk = (); # resetting this prevents data_source referencing # tables from fouling up their fk objects if ($fk_reverse_sth) { while (my $data = $fk_reverse_sth->fetchrow_hashref()) { foreach ( qw( FK_NAME FK_TABLE_NAME FKTABLE_NAME UK_TABLE_NAME PKTABLE_NAME FK_COLUMN_NAME FKCOLUMN_NAME UK_COLUMN_NAME PKCOLUMN_NAME PKTABLE_SCHEM FKTABLE_SCHEM UK_TABLE_SCHEM FK_TABLE_SCHEM) ) { next unless defined($data->{$_}); # Postgres puts quotes around things that look like keywords $data->{$_} =~ s/"|'//g; } my $constraint_name = $data->{'FK_NAME'}; my $fk_table_name = $data->{'FK_TABLE_NAME'} || $data->{'FKTABLE_NAME'}; my $r_table_name = $data->{'UK_TABLE_NAME'} || $data->{'PKTABLE_NAME'}; my $fk_column_name = $data->{'FK_COLUMN_NAME'} || $data->{'FKCOLUMN_NAME'}; my $r_column_name = $data->{'UK_COLUMN_NAME'} || $data->{'PKCOLUMN_NAME'}; my $owner = $data->{'FK_TABLE_SCHEM'} || $data->{'FKTABLE_SCHEM'} || $table_object->owner; my $r_owner = $data->{'UK_TABLE_SCHEM'} || $data->{'PKTABLE_SCHEM'} || $table_object->owner; # MySQL returns primary key info with foreign_key_info()?! # They show up here with no $r_table_name or $r_column_name next unless ($r_table_name and $r_column_name); my $fk = UR::DataSource::RDBMS::FkConstraint->get(fk_constraint_name => $constraint_name, table_name => $fk_table_name, owner => $owner, r_table_name => $r_table_name, r_owner => $r_owner, data_source => $table_object->{'data_source'}, ); unless ($fk) { $fk = UR::DataSource::RDBMS::FkConstraint->$creation_method( fk_constraint_name => $constraint_name, table_name => $fk_table_name, r_table_name => $r_table_name, owner => $owner, r_owner => $r_owner, data_source => $table_object->{data_source}, last_object_revision => $revision_time, ); unless ($fk) { ##$DB::single = 1; 1; } $fk{$fk->fk_constraint_name} = $fk; $data_was_changed_for_this_table = 1; } if ($fk{$fk->fk_constraint_name}) { my %fkcol_params = ( fk_constraint_name => $constraint_name, table_name => $fk_table_name, column_name => $fk_column_name, r_table_name => $r_table_name, r_column_name => $r_column_name, owner => $owner, data_source => $table_object->{data_source}, ); unless ( UR::DataSource::RDBMS::FkConstraintColumn->get(%fkcol_params) ) { UR::DataSource::RDBMS::FkConstraintColumn->$creation_method(%fkcol_params); } } my $fingerprint = $self->_make_foreign_key_fingerprint($fk); $fks_in_real_db{$fingerprint} = $fk; push @constraints, $fk; } } # Find FKs still in the Meta db that don't exist in the real database anymore foreach my $fingerprint ( keys %fks_in_meta_db_by_fingerprint ) { unless ($fks_in_real_db{$fingerprint}) { my $fk = $fks_in_meta_db_by_fingerprint{$fingerprint}; my @fk_cols = $fk->get_related_column_objects(); $_->delete foreach @fk_cols; $fk->delete; } } # get primary_key_info my $pk_sth = $data_source->get_primary_key_details_from_data_dictionary(undef, $ds_owner, $db_table_name); if ($pk_sth) { my @new_pk; while (my $data = $pk_sth->fetchrow_hashref()) { $data->{'COLUMN_NAME'} =~ s/"|'//g; # Postgres puts quotes around things that look like keywords my $pk = UR::DataSource::RDBMS::PkConstraintColumn->get( table_name => $ur_table_name, owner => $ur_owner, data_source => $data_source_id, column_name => $data->{'COLUMN_NAME'}, ); if ($pk) { # Since the rank/order is pretty much all that might change, we # just delete and re-create these. # It's a no-op at save time if there are no changes. $pk->delete; } push @new_pk, [ table_name => $ur_table_name, data_source => $data_source_id, owner => $ds_owner, column_name => $data->{'COLUMN_NAME'}, rank => $data->{'KEY_SEQ'} || $data->{'ORDINAL_POSITION'}, ]; # $table_object->{primary_key_constraint_name} = $data->{PK_NAME}; # $embed{primary_key_constraint_column_names} ||= {}; # $embed{primary_key_constraint_column_names}{$table_object} ||= []; # push @{ $embed{primary_key_constraint_column_names}{$table_object} }, $data->{COLUMN_NAME}; } for my $data (@new_pk) { my $pk = UR::DataSource::RDBMS::PkConstraintColumn->$creation_method(@$data); unless ($pk) { $self->error_message("Failed to create primary key @$data"); return; } } } ## Get the unique constraints ## Unfortunately, there appears to be no DBI catalog ## method which will find these. So we have to use ## some custom SQL # # The SQL that used to live here was moved to the UR::DataSource::Oracle # and each other DataSource class needs its own implementation # The above was moved into each data source's class if (my $uc = $data_source->get_unique_index_details_from_data_dictionary($db_table_name)) { my %uc = %$uc; # make a copy we can manipulate in case $uc is shared or read-only # check for redundant unique constraints # there may be both an index and a constraint for my $uc_name_1 ( keys %uc ) { my $uc_columns_1 = $uc{$uc_name_1} or next; my $uc_columns_1_serial = join ',', sort @$uc_columns_1; for my $uc_name_2 ( keys %uc ) { next if ( $uc_name_2 eq $uc_name_1 ); my $uc_columns_2 = $uc{$uc_name_2} or next; my $uc_columns_2_serial = join ',', sort @$uc_columns_2; if ( $uc_columns_2_serial eq $uc_columns_1_serial ) { delete $uc{$uc_name_1}; } } } # compare primary key constraints to unique constraints my $pk_columns_serial = join(',', sort map { $_->column_name } UR::DataSource::RDBMS::PkConstraintColumn->get( data_source => $data_source_id, table_name => $ur_table_name, owner => $ds_owner, ) ); for my $uc_name ( keys %uc ) { # see if primary key constraint has the same name as # any unique constraints # FIXME - disabling this for now, the Meta DB dosen't track PK constraint names # Isn't it just as goot to check the involved columns? #if ( $table_object->primary_key_constraint_name eq $uc_name ) { # delete $uc{$uc_name}; # next; #} # see if any unique constraints cover the exact same column(s) as # the primary key column(s) my $uc_columns_serial = join ',', sort @{ $uc{$uc_name} }; if ( $pk_columns_serial eq $uc_columns_serial ) { delete $uc{$uc_name}; } } # Create new UniqueConstraintColumn objects for the columns that don't exist, and delete the # objects if they don't apply anymore foreach my $uc_name ( keys %uc ) { my %constraint_objs = map { $_->column_name => $_ } UR::DataSource::RDBMS::UniqueConstraintColumn->get( data_source => $data_source_id, table_name => $ur_table_name, owner => $ds_owner || '', constraint_name => $uc_name, ); foreach my $col_name ( @{$uc{$uc_name}} ) { if ($constraint_objs{$col_name} ) { delete $constraint_objs{$col_name}; } else { my $uc = UR::DataSource::RDBMS::UniqueConstraintColumn->$creation_method( data_source => $data_source_id, table_name => $ur_table_name, owner => $ds_owner, constraint_name => $uc_name, column_name => $col_name, ); 1; } } foreach my $obj ( values %constraint_objs ) { $obj->delete(); } } } $table_object->last_object_revision($revision_time) if ($data_was_changed_for_this_table); # Now that all columns know their foreign key constraints, # have the column objects resolve the various names # associated with the column. #for my $col (@column_objects) { $col->resolve_names } # Determine the ER type. # We have 'validation item', 'entity', and 'bridge' my $column_count = scalar($table_object->column_names) || 0; my $pk_column_count = scalar($table_object->primary_key_constraint_column_names) || 0; my $constraint_count = scalar($table_object->fk_constraint_names) || 0; if ($column_count == 1 and $pk_column_count == 1) { $table_object->er_type('validation item'); } else { if ($constraint_count == $column_count) { $table_object->er_type('bridge'); } else { $table_object->er_type('entity'); } } return $table_object; } sub _make_foreign_key_fingerprint { my($self,$fk) = @_; my @column_objects_with_name = map { [ $_->column_name, $_ ] } $fk->get_related_column_objects(); my @fk_cols = map { $_->[1] } sort {$a->[0] cmp $b->[0]} @column_objects_with_name; my $fingerprint = join(':', $fk->table_name, $fk->r_table_name, map { $_->column_name, $_->r_column_name } @fk_cols ); return $fingerprint; } sub _resolve_owner_and_table_from_table_name { my($self, $table_name) = @_; if ($table_name =~ m/(\w+)\.(\w+)/) { return($1,$2); } else { return($self->owner, $table_name); } } # Derived classes should define a method to return a ref to an array of hash refs # describing all the bitmap indicies in the DB. Each hash ref should contain # these keys: table_name, column_name, index_name # If the DB dosen't support bitmap indicies, it should return an empty listref # This is used by the part that writes class defs based on the DB schema, and # possibly by sync_database() # Implemented methods should take one optional argument: a table name # # FIXME The API for bitmap_index and unique_index methods here aren't the same as # the other data_dictionary methods. These two return hashrefs of massaged # data while the others return DBI statement handles. sub get_bitmap_index_details_from_data_dictionary { my $class = shift; Carp::confess("Class $class didn't define its own bitmap_index_info() method"); } # Derived classes should define a method to return a ref to a hash keyed by constraint # names. Each value holds a listref of hashrefs containing these keys: # CONSTRAINT_NAME and COLUMN_NAME sub get_unique_index_details_from_data_dictionary { my $class = shift; Carp::confess("Class $class didn't define its own unique_index_info() method"); } our %sequence_for_class_name; sub autogenerate_new_object_id_for_class_name_and_rule { # The sequences in the database are named by a naming convention which allows us to connect them to the table # whose surrogate keys they fill. Look up the sequence and get a unique value from it for the object. # If and when we save, we should not get any integrity constraint violation errors. my $self = shift; my $class_name = shift; my $rule = shift; # Not used for the moment... if ($self->use_dummy_autogenerated_ids) { return $self->next_dummy_autogenerated_id; } my $sequence = $sequence_for_class_name{$class_name} || $class_name->__meta__->id_generator; # FIXME Child classes really should use the same sequence generator as its parent # if it doesn't specify its own. # It'll be hard to distinguish the case of a class meta not explicitly mentioning its # sequence name, but there's a sequence generator in the schema for it (the current # mechanism), and when we should defer to the parent's sequence... unless ($sequence) { # This class directly doesn't have a sequence specified. Search through the inheritance my $table_name; for my $parent_class_name ($class_name, $class_name->inheritance) { # print "checking $parent_class_name (for $class_name)\n"; my $parent_class = $parent_class_name->__meta__; # UR::Object::Type->get(class_name => $parent_class_name); # print "object $parent_class\n"; next unless $parent_class; #$sequence = $class_meta->id_generator; #last if $sequence; if ($table_name = $parent_class->table_name) { # print "found table $table_name\n"; last; } } unless ($table_name) { Carp::croak("Could not determine a table name for class $class_name"); } my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my $table_meta = UR::DataSource::RDBMS::Table->get( table_name => $ds_table, owner => $ds_owner, data_source => $self->_my_data_source_id); my @primary_keys; if ($table_meta) { @primary_keys = $table_meta->primary_key_constraint_column_names; $sequence = $self->_get_sequence_name_for_table_and_column($table_name, $primary_keys[0]); } else { # No metaDB info... try and make a guess based on the class' ID properties my $class_meta = $class_name->__meta__; for my $meta ($class_meta, $class_meta->ancestry_class_metas) { @primary_keys = grep { $_ } # Only interested in the properties with columns defined map { $_->column_name } $meta->direct_id_property_metas; if (@primary_keys > 1) { Carp::croak("Tables with multiple primary keys (i.e. " . $table_name . ": " . join(',',@primary_keys) . ") cannot have a surrogate key created from a sequence."); } elsif (@primary_keys == 1) { $sequence = $self->_get_sequence_name_for_table_and_column($table_name, $primary_keys[0]); last if $sequence; } } } if (@primary_keys == 0) { Carp::croak("No primary keys found for table " . $table_name . "\n"); } if (!$sequence) { Carp::croak("No identity generator found for table " . $table_name . "\n"); } $sequence_for_class_name{$class_name} = $sequence; } my $new_id = $self->_get_next_value_from_sequence($sequence); return $new_id; } sub _get_sequence_name_for_table_and_column { my($self,$table_name,$column_name) = @_; # The default is to take the column name (should be a primary key from a table) and # change the _ID at the end of the column name with _SEQ # if column_name is all uppercase, make the sequence name end in upper case _SEQ my $replacement = $column_name eq uc($column_name) ? '_SEQ' : '_seq'; $column_name =~ s/_ID/$replacement/i; return $column_name; } sub resolve_order_by_clause { my($self,$order_by_columns,$order_by_column_data) = @_; my @cols = @$order_by_columns; foreach my $col ( @cols) { if ($col =~ m/^(-|\+)(.*)$/) { $col = $2; if ($1 eq '-') { $col = $col . ' DESC'; } } } return 'order by ' . join(', ',@cols); } sub create_iterator_closure_for_rule { my ($self, $rule) = @_; my ($rule_template, @values) = $rule->template_and_values(); my $query_plan = $self->_resolve_query_plan($rule_template); # # the template has general class data # my $class_name = $query_plan->{class_name}; my $class = $class_name; my @lob_column_names = @{ $query_plan->{lob_column_names} }; my @lob_column_positions = @{ $query_plan->{lob_column_positions} }; my $query_config = $query_plan->{query_config}; my $post_process_results_callback = $query_plan->{post_process_results_callback}; # # the template has explicit template data # my $select_clause = $query_plan->{select_clause}; my $select_hint = $query_plan->{select_hint}; my $from_clause = $query_plan->{from_clause}; my $where_clause = $query_plan->{where_clause}; my $connect_by_clause = $query_plan->{connect_by_clause}; my $group_by_clause = $query_plan->{group_by_clause}; my $order_by_columns = $query_plan->{order_by_columns} || []; my $sql_params = $query_plan->{sql_params}; my $filter_specs = $query_plan->{filter_specs}; my @property_names_in_resultset_order = @{ $query_plan->{property_names_in_resultset_order} }; # TODO: we get 90% of the way to a full where clause in the template, but # actually have to build it here since ther is no way to say "in (?)" and pass an arrayref :( # It _is_ possible, however, to process all of the filter specs with a constant number of params. # This would optimize the common case. my @all_sql_params = @$sql_params; for my $filter_spec (@$filter_specs) { my ($expr_sql, $operator, $value_position) = @$filter_spec; my $value = $values[$value_position]; my ($more_sql, @more_params) = $self->_extend_sql_for_column_operator_and_value($expr_sql, $operator, $value); $where_clause .= ($where_clause ? "\nand " : ($connect_by_clause ? "start with " : "where ")); if ($more_sql) { $where_clause .= $more_sql; push @all_sql_params, @more_params; } else { # error return; } } # The full SQL statement for the template, besides the filter logic, is built here. my $order_by_clause; if (@$order_by_columns) { $order_by_clause = $self->resolve_order_by_clause($order_by_columns,$query_plan->_order_by_property_names); } my $sql = "\nselect "; if ($select_hint) { my $hint = ''; foreach (@$select_hint) { $hint .= ' ' . $_; } $hint =~ s/\/\*\s?|\s?\*\///g; # remove embedded comment marks $sql .= "/*$hint */ "; } $sql .= $select_clause; $sql .= "\nfrom $from_clause"; $sql .= "\n$where_clause" if defined($where_clause) and length($where_clause); $sql .= "\n$connect_by_clause" if $connect_by_clause; $sql .= "\n$group_by_clause" if $group_by_clause; $sql .= "\n$order_by_clause" if $order_by_clause; $self->__signal_change__('query',$sql); my $dbh = $self->get_default_handle; my $sth = $dbh->prepare($sql,$query_config); unless ($sth) { $class->error_message("Failed to prepare SQL $sql\n" . $dbh->errstr . "\n"); Carp::confess($class->error_message); } unless ($sth->execute(@all_sql_params)) { $class->error_message("Failed to execute SQL $sql\n" . $sth->errstr . "\n" . Data::Dumper::Dumper(\@all_sql_params) . "\n"); Carp::confess($class->error_message); } die unless $sth; # FIXME - this has no effect, right? # buffers for the iterator my $next_db_row; my $pending_db_object_data; my $ur_test_filldb = $ENV{'UR_TEST_FILLDB'}; my $iterator = sub { unless ($sth) { ##$DB::single = 1; return; } $next_db_row = $sth->fetchrow_arrayref; #$self->__signal_change__('fetch',$next_db_row); # FIXME: commented out because it may make fetches too slow unless ($next_db_row) { $sth->finish; $sth = undef; return; } # this handles things like BLOBS, which have a special interface to get the 'real' data if ($post_process_results_callback) { $next_db_row = $post_process_results_callback->($next_db_row); } # this is used for automated re-testing against a private database $self->_CopyToAlternateDB($class,$dbh,$next_db_row) if $ur_test_filldb; return $next_db_row; }; # end of iterator closure Sub::Name::subname('UR::DataSource::RDBMS::__datasource_iterator(closure)__', $iterator); return $iterator; } sub _default_sql_like_escape_string { return '\\'; # Most RDBMSs support an 'escape' as part of a 'like' operator, except mysql } sub _format_sql_like_escape_string { my $class = shift; my $escape = shift; return "'$escape'"; } # This allows the size of an autogenerated IN-clause to be adjusted. # The limit for Oracle is 1000, and a bug requires that, in some cases # we drop to 250. my $in_clause_size_limit = 250; # This method is used when generating SQL for a rule template, in the joins # and also on a per-query basis to turn specific values into a where clause sub _extend_sql_for_column_operator_and_value { my ($self, $expr_sql, $op, $val, $escape) = @_; $op ||= ''; if ($op eq 'in' and not ref($val) eq 'ARRAY') { ##$DB::single = 1; $val = []; } my $sql; my @sql_params; if ($op eq '' or $op eq '=' or $op eq 'eq') { $sql .= $expr_sql; if ($self->_value_is_null($val)) { $sql = "$expr_sql is NULL"; } else { $sql = "$expr_sql = ?"; push @sql_params, $val; } } elsif ($op =~ m/^between( \[\])?/i) { $sql .= "$expr_sql between ? and ?"; push @sql_params, @$val; } elsif ($op =~ /\[\]/ or $op =~ /in/i) { no warnings 'uninitialized'; my $not = $op =~ m/not/i; unless (@$val) { # an empty list was passed-in. # since "in ()", like "where 1=0", is self-contradictory, # there is no data to return, and no SQL required $self->warning_message(Carp::shortmess("Null in-clause passed to default_load_sql")); return; } my @list = sort @$val; my $has_null = ( (grep { length($_) == 0 } @list) ? 1 : 0); my $wrap = ($has_null or @$val > $in_clause_size_limit ? 1 : 0); my $cnt = 0; $sql .= "\n(\n " if $wrap; my $dbh = $self->get_default_handle; while (my @set = splice(@list,0,$in_clause_size_limit)) { $sql .= "\n or " if $cnt++; $sql .= $expr_sql; $sql .= ' not ' if $not; $sql .= " in (" . join(",",map { $dbh->quote($_) } @set) . ")"; } if ($has_null) { $sql .= "\n or $expr_sql is "; $sql .= 'not' if ($not); $sql .= ' null'; } $sql .= "\n)\n" if $wrap; } elsif($op =~ /^(like|not like|in|not in|\<\>|\<|\>|\=|\<\=|\>\=)$/i ) { # SQL operator. Use this directly. $sql .= "$expr_sql $op ?"; push @sql_params, $val; if($op =~ /like/i and my $default_escape = $self->_default_sql_like_escape_string) { $escape ||= $default_escape; $escape = $self->_format_sql_like_escape_string($escape); $sql .= " escape $escape"; } } elsif($op =~ /^(ne|\!\=)$/i) { # Perlish inequality. Special SQL to handle this. if (not defined($val)) { # ne undef =~ is not null $sql .= "$expr_sql is not null"; pop @sql_params; } elsif ($op =~ /^(ne|\!\=)$/i) { # ne $v =~ should match everything but $v, including nulls # != is the same, and will rely on is_loaded to # filter out any cases where "hello" != "goodbye" returns # but Perl wants to exclude the value because they match numerically. $sql .= "( $expr_sql != ?" . " or $expr_sql is null)"; push @sql_params, $val; } } elsif ($op eq 'true' ) { $sql .= "( $expr_sql is not null and $expr_sql != 0 )"; } elsif ($op eq 'false' ) { $sql .= "( $expr_sql is null or $expr_sql = 0)"; } else { # Something else? die "Unknown operator $op!"; } if (@sql_params > 256) { Carp::confess("A bug in Oracle causes queries using > 256 placeholders to return incorrect results."); } return ($sql, @sql_params) } sub _value_is_null { # this is a separate method since some databases, like Oracle, treat empty strings as null values my ($self, $value) = @_; return 1 if not defined $value; return if not ref($value); if (ref($value) eq 'HASH') { if ($value->{operator} eq '=' or $value->{operator} eq 'eq') { if (not defined $value->{value}) { return 1; } else { return; } } } return; } sub _resolve_ids_from_class_name_and_sql { my $self = shift; my $class_name = shift; my $sql = shift; my $query; my @params; if (ref($sql) eq "ARRAY") { ($query, @params) = @{$sql}; } else { $query = $sql; } my $class_meta = $class_name->__meta__; my @id_columns = map { $class_meta->property_meta_for_name($_)->column_name } $class_meta->id_property_names; # query for the ids my $dbh = $self->get_default_handle(); my $sth = $dbh->prepare($query); unless ($sth) { Carp::croak("Could not prepare query $query: $DBI::errstr"); } unless ($sth->{NUM_OF_PARAMS} == scalar(@params)) { Carp::croak('The number of params supplied (' . scalar(@params) . ') does not match the number of placeholders (' . $sth->{NUM_OF_PARAMS} . ") in the supplied sql: $query"); } $sth->execute(@params); # After execute, we can see if the SQL contained all the required primary keys my @id_column_idx = map { $sth->{NAME_lc_hash}->{$_} } map { lc } @id_columns; if (grep { ! defined } @id_column_idx) { @id_columns = sort @id_columns; my @missing_ids = sort grep { ! defined($sth->{NAME_lc_hash}->{lc($_)}) } @id_columns; Carp::croak("The SQL supplied is missing one or more ID columns.\n\tExpected: " . join(', ', @id_columns) . ' but some were missing: ' . join(', ', @missing_ids) . " for query: $query"); } my $id_resolver = $class_name->__meta__->get_composite_id_resolver(); my $id_values = $sth->fetchall_arrayref(\@id_column_idx); return [ map { $id_resolver->(@$_) } @$id_values ]; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $changed_objects = delete $params{changed_objects}; my %objects_by_class_name; for my $obj (@$changed_objects) { my $class_name = ref($obj); $objects_by_class_name{$class_name} ||= []; push @{ $objects_by_class_name{$class_name} }, $obj; } my $dbh = $self->get_default_handle; # # Determine what commands need to be executed on the database # to sync those changes, and categorize them by type and table. # # As we iterate through changes, keep track of all of the involved tables. my %all_tables; # $all_tables{$table_name} = $number_of_commands; # Make a hash for each type of command keyed by table name. my %insert; # $insert{$table_name} = [ $change1, $change2, ...]; my %update; # $update{$table_name} = [ $change1, $change2, ...]; my %delete; # $delete{$table_name} = [ $change1, $change2, ...]; # Make a master hash referencing each of the above. # $explicit_commands_by_type_and_table{'insert'}{$table} = [ $change1, $change2 ...] my %explicit_commands_by_type_and_table = ( 'insert' => \%insert, 'update' => \%update, 'delete' => \%delete ); # Build the above data structures. { no warnings; for my $class_name (sort keys %objects_by_class_name) { for my $obj (@{ $objects_by_class_name{$class_name} }) { my @commands = $self->_default_save_sql_for_object($obj); next unless @commands; for my $change (@commands) { #$commands{$change} = $change; # Example change: # { type => 'update', table_name => $table_name, # column_names => \@changed_cols, sql => $sql, # params => \@values, class => $table_class, id => $id }; # There are often multiple changes per object, espeically # when the object is spread across multiple tables because of # inheritance. We classify each change by the table and # the class immediately associated with the table, even if # the class in an abstract parent class on the object. my $table_name = $change->{table_name}; my $id = $change->{id}; $all_tables{$table_name}++; my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my $table = $self->_get_table_object($ds_table, $ds_owner); my $fully_qualified_table_name = defined $ds_owner ? join('.', $ds_owner, $ds_table) : $ds_table; if ($change->{type} eq 'insert') { push @{ $insert{$fully_qualified_table_name} }, $change; } elsif ($change->{type} eq 'update') { push @{ $update{$fully_qualified_table_name} }, $change; } elsif ($change->{type} eq 'delete') { push @{ $delete{$fully_qualified_table_name} }, $change; } else { print "UNKNOWN COMMAND TYPE $change->{type} $change->{sql}\n"; } } } } } # Determine which tables require a lock; my %tables_requiring_lock; for my $table_name (keys %all_tables) { my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my $table_object = $self->_get_table_object($ds_table, $ds_owner); unless ($table_object) { warn "looking up schema for RDBMS table $table_name...\n"; $table_object = $self->refresh_database_metadata_for_table_name($table_name); unless ($table_object) { die "Failed to generate table data for $table_name!"; } } if (my @bitmap_index_names = $table_object->bitmap_index_names) { my $changes; if ($changes = $insert{$table_name} or $changes = $delete{$table_name}) { $tables_requiring_lock{$table_name} = 1; } elsif (not $tables_requiring_lock{$table_name}) { $changes = $update{$table_name}; my @column_names = sort map { @{ $_->{column_names} } } @$changes; my $last_column_name = ""; for my $column_name (@column_names) { next if $column_name eq $last_column_name; my $column_obj = UR::DataSource::RDBMS::TableColumn->get( data_source => $table_object->data_source, table_name => $ds_table, owner => $ds_owner, column_name => $column_name, ); if ($column_obj->bitmap_index_names) { $tables_requiring_lock{$table_name} = 1; last; } $last_column_name = $column_name; } } } } # # Make a mapping of prerequisites for each command, # and a reverse mapping of dependants for each command. # my %all_table_commands; my %prerequisites; my %dependants; for my $table_name_from_class (keys %all_tables) { my($ds_owner,$ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name_from_class); my $data_source_id = $self->_my_data_source_id; my $table = $self->_get_table_object($ds_table, $ds_owner); my @fk = $table->fk_constraints; my $table_name = defined $ds_owner ? join('.', $ds_owner, $ds_table) : $ds_table; my $matched_table_name; if ($insert{$table_name}) { $matched_table_name = 1; $all_table_commands{"insert $table_name"} = 1; } if ($update{$table_name}) { $matched_table_name = 1; $all_table_commands{"update $table_name"} = 1; } if ($delete{$table_name}) { $matched_table_name = 1; $all_table_commands{"delete $table_name"} = 1; } unless ($matched_table_name) { Carp::carp("Possible metadata inconsistency: A change on table $table_name was not an insert, update or delete!"); } my $tmparray; # handle multiple differnt ops on the same table if ($insert{$table_name} and $update{$table_name}) { # insert before update $tmparray = $prerequisites{"update $table_name"}{"insert $table_name"} ||= []; $tmparray = $dependants{"insert $table_name"}{"update $table_name"} ||= []; } if ($delete{$table_name} and $update{$table_name}) { # update before delete $tmparray = $prerequisites{"delete $table_name"}{"update $table_name"} ||= []; $tmparray = $dependants{"update $table_name"}{"delete $table_name"} ||= []; } if ($delete{$table_name} and $insert{$table_name} and not $update{$table_name}) { # delete before insert $tmparray = $prerequisites{"insert $table_name"}{"delete $table_name"} ||= []; $tmparray = $dependants{"delete $table_name"}{"insert $table_name"} ||= []; } # Go through the constraints. for my $fk (@fk) { my $r_table = $fk->r_table_name; my $r_owner = $fk->r_owner; my $r_table_name = defined $r_owner ? join('.', $r_owner, $r_table) : $r_table; # RULES: # insert r_table_name before insert table_name # insert r_table_name before update table_name # delete table_name before delete r_table_name # update table_name before delete r_table_name if ($insert{$table_name} and $insert{$r_table_name}) { $tmparray = $prerequisites{"insert $table_name"}{"insert $r_table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"insert $r_table_name"}{"insert $table_name"} ||= []; push @$tmparray, $fk; } if ($update{$table_name} and $insert{$r_table_name}) { $tmparray = $prerequisites{"update $table_name"}{"insert $r_table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"insert $r_table_name"}{"update $table_name"} ||= []; push @$tmparray, $fk; } if ($delete{$r_table_name} and $delete{$table_name}) { $tmparray = $prerequisites{"delete $r_table_name"}{"delete $table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"delete $table_name"}{"delete $r_table_name"} ||= []; push @$tmparray, $fk; } if ($delete{$r_table_name} and $update{$table_name}) { $tmparray = $prerequisites{"delete $r_table_name"}{"update $table_name"} ||= []; push @$tmparray, $fk; $tmparray = $dependants{"update $table_name"}{"delete $r_table_name"} ||= []; push @$tmparray, $fk; } } } # # Use the above mapping to build an ordered list of general commands. # Note that the general command is something like "insert EMPLOYEES", # while the explicit command is an exact insert statement with params. # my @general_commands_in_order; my %self_referencing_table_commands; my %all_unresolved = %all_table_commands; my $unresolved_count; my $last_unresolved_count = 0; my @ready_to_add = (); while ($unresolved_count = scalar(keys(%all_unresolved))) { if ($unresolved_count == $last_unresolved_count) { # We accomplished nothing on the last iteration. # We are in an infinite loop unless something is done. # Rather than die with an error, issue a warning and attempt to # brute-force the sync. # Process something with minimal deps as a work-around. my @ordered_by_least_number_of_prerequisites = sort{ scalar(keys(%{$prerequisites{$a}})) <=> scalar(keys(%{$prerequisites{$b}})) } grep { $prerequisites{$_} } keys %all_unresolved; @ready_to_add = ($ordered_by_least_number_of_prerequisites[0]); warn "Circular dependency! Pushing @ready_to_add to brute-force the save.\n"; #print STDERR Data::Dumper::Dumper(\%objects_by_class_name, \%prerequisites, \%dependants ) . "\n"; } else { # This is the normal case. It is either the first iteration, # or we are on additional iterations with some progress made # in the last iteration. # Find commands which have no unresolved prerequisites. @ready_to_add = grep { not $prerequisites{$_} } keys %all_unresolved; # If there are none of the above, find commands # with only self-referencing prerequisites. unless (@ready_to_add) { # Find commands with only circular dependancies. @ready_to_add = # The circular prerequisite must be the only prerequisite on the table. grep { scalar(keys(%{$prerequisites{$_}})) == 1 } # The prerequisite must be the same as the the table itself. grep { $prerequisites{$_}{$_} } # There must be prerequisites for the given table, grep { $prerequisites{$_} } # Look at all of the unresolved table commands. keys %all_unresolved; # Note this for below. # It records the $fk object which is circular. for my $table_command (@ready_to_add) { $self_referencing_table_commands{$table_command} = $prerequisites{$table_command}{$table_command}; } } } # Record our current unresolved count for comparison on the next iteration. $last_unresolved_count = $unresolved_count; for my $db_command (@ready_to_add) { # Put it in the list. push @general_commands_in_order, $db_command; # Delete it from the main hash of command/table pairs # for which dependencies are not resolved. delete $all_unresolved{$db_command}; # Find anything which depended on this command occurring first # and remove this command from that command's prerequisite list. for my $dependant (keys %{ $dependants{$db_command} }) { # Tell it to take us out of its list of prerequisites. delete $prerequisites{$dependant}{$db_command} if $prerequisites{$dependant}; # Get rid of the prereq entry if it is empty; delete $prerequisites{$dependant} if (keys(%{ $prerequisites{$dependant} }) == 0); } # Note that nothing depends on this command any more since it has been queued. delete $dependants{$db_command}; } } # Go through the ordered list of general commands (ie "insert TABLE_NAME") # and build the list of explicit commands. my @explicit_commands_in_order; for my $general_command (@general_commands_in_order) { my ($dml_type,$table_name) = split(/\s+/,$general_command); if (my $circular_fk_list = $self_referencing_table_commands{$general_command}) { # A circular foreign key requires that the # items be inserted in a specific order. my (@rcol_sets) = map { [ $_->column_names ] } @$circular_fk_list; # Get the IDs and objects which need to be saved. my @cmds = @{ $explicit_commands_by_type_and_table{$dml_type}{$table_name} }; my @ids = map { $_->{id} } @cmds; # my @objs = $cmds[0]->{class}->is_loaded(\@ids); my $is_loaded_class = ($dml_type eq 'delete') ? $cmds[0]->{class}->ghost_class : $cmds[0]->{class}; my @objs = $is_loaded_class->is_loaded(\@ids); my %objs = map { $_->id => $_ } @objs; # Produce the explicit command list in dep order. my %unsorted_cmds = map { $_->{id} => $_ } @cmds; my $add; my @local_explicit_commands; my %adding; $add = sub { my ($cmd) = @_; if ($adding{$cmd}) { ##$DB::single = 1; Carp::confess("Circular foreign key!") unless $main::skip_croak; } $adding{$cmd} = 1; my $obj = $objs{$cmd->{id}}; my $class_meta = $obj->class->__meta__; for my $rcol_set (@rcol_sets) { my @ordered_values = map { $obj->$_ } map { $class_meta->property_for_column($_) } @$rcol_set; my $pid = $obj->class->__meta__->resolve_composite_id_from_ordered_values(@ordered_values); if (defined $pid) { # This recursive foreign key dep may have been optional my $pcmd = delete $unsorted_cmds{$pid}; $add->($pcmd) if $pcmd; } } delete $adding{$cmd}; push @local_explicit_commands, $cmd; }; for my $cmd (@cmds) { next unless $unsorted_cmds{$cmd->{id}}; $add->(delete $unsorted_cmds{$cmd->{id}}); } if ($dml_type eq 'delete') { @local_explicit_commands = reverse @local_explicit_commands; } push @explicit_commands_in_order, @local_explicit_commands; } else { # Order is irrelevant on non-self-referencing tables. push @explicit_commands_in_order, @{ $explicit_commands_by_type_and_table{$dml_type}{$table_name} }; } } my %table_objects_by_class_name; my %column_objects_by_class_and_column_name; # Make statement handles. my %sth; for my $cmd (@explicit_commands_in_order) { my $sql = $cmd->{sql}; unless ($sth{$sql}) { my $class_name = $cmd->{class}; # get the db handle to use for this class my $dbh = $cmd->{'dbh'}; #$class_name->dbh; my $sth = $dbh->prepare($sql); $sth{$sql} = $sth; if ($dbh->errstr) { $self->error_message("Error preparing SQL:\n$sql\n" . $dbh->errstr . "\n"); return; } my $tables = $table_objects_by_class_name{$class_name}; my $class_object = $class_name->__meta__; unless ($tables) { my $tables; my @all_table_names = $class_object->all_table_names; for my $table_name (@all_table_names) { my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my $data_source_id = $self->_my_data_source_id; my $table = $self->_get_table_object($ds_table, $ds_owner); push @$tables, $table; $column_objects_by_class_and_column_name{$class_name} ||= {}; my $columns = $column_objects_by_class_and_column_name{$class_name}; unless (%$columns) { for my $column ($table->columns) { $columns->{$column->column_name} = $column; } } } $table_objects_by_class_name{$class_name} = $tables; } my @column_objects; foreach my $column_name ( @{ $cmd->{column_names} } ) { my $column = $column_objects_by_class_and_column_name{$class_name}->{$column_name}; unless ($column) { FIND_IN_ANCESTRY: for my $ancestor_class_name ($class_object->ancestry_class_names) { $column = $column_objects_by_class_and_column_name{$ancestor_class_name}->{$column_name}; if ($column) { $column_objects_by_class_and_column_name{$class_name}->{$column_name} = $column; last FIND_IN_ANCESTRY; } } } # If we didn't find a column object, then $column will be undef # and we'll have to guess what it looks like push @column_objects, $column; } # print "Column Types: @column_types\n"; $self->_alter_sth_for_selecting_blob_columns($sth,\@column_objects); } } # Set a savepoint if possible. my $savepoint; if ($self->can_savepoint) { $savepoint = $self->_last_savepoint; if ($savepoint) { $savepoint++; } else { $savepoint=1; } my $sp_name = "sp".$savepoint; unless ($self->set_savepoint($sp_name)) { $self->error_message("Failed to set a savepoint on " . $self->class . ": " . $dbh->errstr ); return; } $self->_last_savepoint($savepoint); } else { # FIXME SQLite dosen't support savepoints, but autocommit is already off so this dies?! #$dbh->begin_work; } # Do any explicit table locking necessary. if (my @tables_requiring_lock = sort keys %tables_requiring_lock) { $self->debug_message("Locking tables: @tables_requiring_lock."); my $max_failed_attempts = 10; for my $table_name (@tables_requiring_lock) { my($ds_owner, $ds_table) = $self->_resolve_owner_and_table_from_table_name($table_name); my $table = $self->_get_table_object($ds_table, $ds_owner); my $dbh = $table->dbh; my $sth = $dbh->prepare("lock table $table_name in exclusive mode"); my $failed_attempts = 0; my @err; for (1) { unless ($sth->execute) { $failed_attempts++; $self->warning_message( "Failed to lock $table_name (attempt # $failed_attempts): " . $sth->errstr ); push @err, $sth->errstr; unless ($failed_attempts >= $max_failed_attempts) { redo; } } } if ($failed_attempts > 1) { my $err = join("\n",@err); #$UR::Context::current->send_email( # To => 'example@example.edu', # From => UR::Context::Process->prog_name . ' ', # Subject => ( # $failed_attempts >= $max_failed_attempts # ? "sync_database lock failure after $failed_attempts attempts" # : "sync_database lock success after $failed_attempts attempts" # ) # . " in " . UR::Context::Process->prog_name # . " on $table_name", # Message => qq/ # $failed_attempts attempts to lock table $table_name # # Errors: # $err # # The complete table lock list for this sync: # @tables_requiring_lock # / #); if ($failed_attempts >= $max_failed_attempts) { $self->error_message( "Could not obtain an exclusive table lock on table " . $table_name . " after $failed_attempts attempts" ); $self->rollback_to_savepoint($savepoint); return; } } } } # Execute the commands in the correct order. my @failures; my $last_failure_count = 0; my @previous_failure_sets; # If there are failures, we fall-back to brute force and send # a message to support to debug the inefficiency. my $skip_fault_tolerance_check = 1; for (1) { @failures = (); for my $cmd (@explicit_commands_in_order) { unless ($sth{$cmd->{sql}}->execute(@{$cmd->{params}})) { #my $dbh = $cmd->{class}->dbh; # my $dbh = UR::Context->resolve_data_source_for_object($cmd->{class})->get_default_handle; push @failures, {cmd => $cmd, error_message => $sth{$cmd->{sql}}->errstr}; last if $skip_fault_tolerance_check; } $sth{$cmd->{sql}}->finish(); } if (@failures) { # There have been some failures. In case the error has to do with # a failure to correctly determine dependencies in the code above, # we will retry the set of failed commands. This repeats as long # as some progress is made on each iteration. if ( (@failures == $last_failure_count) or $skip_fault_tolerance_check) { # We've tried this exact set of comands before and failed. # This is a real error. Stop retrying and report. for my $error (@failures) { $self->error_message($self->id . ": Error executing SQL:\n$error->{cmd}{sql}\n" . "PARAMS: " . join(', ',map { defined($_) ? "'$_'" : '(undef)' } @{$error->{cmd}{params}}) . "\n" . $error->{error_message} . "\n"); } last; } else { # We've failed, but we haven't retried this exact set of commands # and found the exact same failures. This is either the first failure, # or we had failures before and had success on the last brute-force # approach to sorting commands. Try again. push @previous_failure_sets, \@failures; @explicit_commands_in_order = map { $_->{cmd} } @failures; $last_failure_count = scalar(@failures); $self->warning_message("RETRYING SAVE"); redo; } } } # Rollback to savepoint if there are errors. if (@failures) { if (!$savepoint or $savepoint eq "NONE") { # A failure on a database which does not support savepoints. # We must rollback the entire transacation. # This is only a problem for a mixed raw-sql and UR::Object environment. $dbh->rollback; } else { $self->_reverse_sync_database(); } # Return false, indicating failure. return; } unless ($self->_set_specified_objects_saved_uncommitted($changed_objects)) { Carp::confess("Error setting objects to a saved state after sync_database. Exiting."); return; } if (exists $params{'commit_on_success'} and ($params{'commit_on_success'} eq '1')) { # Commit the current transaction. # The handles will automatically update their objects to # a committed state from the one set above. # It will throw an exception on failure. $dbh->commit; } # Though we succeeded, see if we had to use the fault-tolerance code to # do so, and warn software support. This should never occur. if (@previous_failure_sets) { my $msg = "Dependency failure saving: " . Dumper(\@explicit_commands_in_order) . "\n\nThe following error sets were produced:\n" . Dumper(\@previous_failure_sets) . "\n\n" . Carp::cluck() . "\n\n"; $self->warning_message($msg); $UR::Context::current->send_email( To => UR::Context::Process->support_email, Subject => 'sync_database dependency sort failure', Message => $msg ) or $self->warning_message("Failed to send error email!"); } return 1; } # this is necessary for overriding data source names when looking up table metadata with # bifurcated oracle/postgres syncs in testing. sub _my_data_source_id { my $self = shift; return ref($self) ? $self->id : $self; } sub _get_table_object { my $self = shift; my ($ds_table, $ds_owner) = @_; my $data_source_id = $self->_my_data_source_id; my $table = UR::DataSource::RDBMS::Table->get( table_name => $ds_table, owner => $ds_owner, data_source => $data_source_id) || UR::DataSource::RDBMS::Table->get( table_name => $ds_table, owner => $ds_owner, data_source => 'UR::DataSource::Meta'); } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; return; } sub _reverse_sync_database { my $self = shift; unless ($self->can_savepoint) { # This will not respect manual DML # Developers must not use this back door on non-savepoint databases. $self->get_default_handle->rollback; return "NONE"; } my $savepoint = $self->_last_savepoint; unless ($savepoint) { Carp::confess("No savepoint set!"); } my $sp_name = "sp".$savepoint; unless ($self->rollback_to_savepoint($sp_name)) { $self->error_message("Error removing savepoint $savepoint " . $self->get_default_handle->errstr); return 1; } $self->_last_savepoint(undef); return $savepoint; } # Given a table object and a list of primary key values, return # a where clause to match a row. Some values may be undef (NULL) # and it properly writes "column IS NULL". As a side effect, the # @$values list is altered to remove the undef value sub _matching_where_clause { my($self,$table_obj,$values) = @_; unless ($table_obj) { Carp::confess("No table passed to _matching_where_clause for $self!"); } my @pks = $table_obj->primary_key_constraint_column_names; my @where; # in @$values, the updated data values always seem to be before the where clause # values but still in the right order, so start at the right place my $skip = scalar(@$values) - scalar(@pks); for (my($pk_idx,$values_idx) = (0,$skip); $pk_idx < @pks;) { if (defined $values->[$values_idx]) { push(@where, $pks[$pk_idx] . ' = ?'); $pk_idx++; $values_idx++; } else { push(@where, $pks[$pk_idx] . ' IS NULL'); splice(@$values, $values_idx, 1); $pk_idx++; } } return join(' and ', @where); } sub _id_values_for_primary_key { my ($self,$table_obj,$object_to_save) = @_; unless ($table_obj && $object_to_save) { Carp::confess("Both table and object_to_save should be passed for $self!"); } my $class_obj; # = $object_to_save->__meta__; foreach my $possible_class_obj ($object_to_save->__meta__->all_class_metas) { next unless ($possible_class_obj->table_name); my($class_owner,$class_table) = $self->_resolve_owner_and_table_from_table_name($possible_class_obj->table_name); # Some data sources can (used to?) have NULL owner/schema $class_owner = '' unless defined($class_owner); my $table_obj_owner = $table_obj->owner; $table_obj_owner = '' unless defined ($table_obj_owner); if ( $class_owner eq $table_obj_owner and $class_table eq $table_obj->table_name ) { $class_obj = $possible_class_obj; last; } } unless (defined $class_obj) { Carp::croak("Can't find class object with table " . $table_obj->table_name . " while searching inheritance for object of class ".$self->class); } my @pk_cols = $table_obj->primary_key_constraint_column_names; my %pk_cols = map { $_ => 1 } @pk_cols; # this previously went to $object_to_save->__meta__, which is nearly the same thing but not quite my @values = $class_obj->resolve_ordered_values_from_composite_id($object_to_save->id); my @columns = $class_obj->direct_id_column_names; foreach my $col_in_class ( @columns ) { unless ($pk_cols{$col_in_class}) { my $table_name = $table_obj->table_name; my $class_name = $class_obj->class_name; Carp::croak("While committing, metadata for table $table_name does not match class $class_name.\n Table primary key columns are " . join(', ',@pk_cols) . "\n class ID property columns " . join(', ', @columns)); } } my $i=0; my %column_index = map { $_ => $i++ } @columns; my @bad_pk_cols = grep { ! exists($column_index{$_}) } @pk_cols; if (@bad_pk_cols) { my $table_name = $table_obj->table_name; Carp::croak("Metadata for table $table_name is inconsistent with class ".$class_obj->class_name.".\n" . "Column(s) named " . join(',',@bad_pk_cols) . " appear as primary key constraint columns, " . "but do not appear as ID column names. Check the dd_pk_constraint_columns data in the " . "MetaDB and the ID properties of the class definition"); } my @id_values_in_pk_order = @values[@column_index{@pk_cols}]; return @id_values_in_pk_order; } sub _lookup_class_for_table_name { my $self = shift; my $table_name = shift; my @table_class_obj = grep { $_->class_name !~ /::Ghost$/ } UR::Object::Type->is_loaded(table_name => $table_name); my $table_class; my $table_class_obj; if (@table_class_obj == 1) { $table_class_obj = $table_class_obj[0]; return $table_class_obj->class_name; } elsif (@table_class_obj > 1) { Carp::confess("Got more than one class object for $table_name, this should not happen: @table_class_obj"); } } sub _default_save_sql_for_object { my $self = shift; my $object_to_save = shift; my %params = @_; my ($class,$id) = ($object_to_save->class, $object_to_save->id); my $class_object = $object_to_save->__meta__; # This object may have uncommitted changes already saved. # If so, work from the last saved data. # Normally, we go with the last committed data. my $compare_version = ($object_to_save->{'db_saved_uncommitted'} ? 'db_saved_uncommitted' : 'db_committed'); # Determine what the overall save action for the object is, # and get a specific change summary if we're doing an update. my ($action,$change_summary); if ($object_to_save->isa('UR::Object::Ghost')) { $action = 'delete'; } elsif ($object_to_save->{$compare_version}) { $action = 'update'; $change_summary = $object_to_save->property_diff($object_to_save->{$compare_version}); } else { $action = 'insert'; } # Handle each table. There is usually only one, unless, # there is inheritance within the schema. my @save_table_names = grep { not /[^\w\.]/ } # remove any views from the list $class_object->all_table_names; @save_table_names = reverse @save_table_names unless ($object_to_save->isa('UR::Entity::Ghost')); my @commands; for my $table_name (@save_table_names) { my ($db_owner, $table_name_to_update) = $self->_resolve_owner_and_table_from_table_name($table_name); # Get general info on the table we're working-with. my $dsn = ref($self) ? $self->_my_data_source_id: $self; # The data source name my $table = $self->_get_table_object($table_name_to_update, $db_owner); unless ($table) { $self->generate_schema_for_class_meta($class_object,1); # try again... $table = $self->_get_table_object($table_name_to_update, $db_owner); unless ($table) { Carp::croak("No table $table_name found for data source $dsn and owner '$db_owner'"); } } my $table_class = $self->_lookup_class_for_table_name($table_name); if (!$table_class) { Carp::croak("NO CLASS FOR $table_name\n"); } my $data_source = $UR::Context::current->resolve_data_source_for_object($object_to_save); unless ($data_source) { Carp::croak("Couldn't resolve data source for object ".$object_to_save->__display_name__.":\n" . Data::Dumper::Dumper($object_to_save)); } # The "action" now can vary on a per-table basis. my $table_action = $action; # Handle re-classification of objects. # We skip deletion and turn insert into update in these cases. if ( ($table_class ne $class) and ( ($table_class . "::Ghost") ne $class) ) { if ($action eq 'delete') { # see if the object we're deleting actually exists reclassified my $replacement = $table_class->is_loaded($id); if ($replacement) { next; } } elsif ($action eq 'insert') { # see if the object we're inserting is actually a reclassification # of a pre-existing object my $replacing = $table_class->ghost_class->is_loaded($id); if ($replacing) { $table_action = 'update'; $change_summary = $object_to_save->property_diff(%$replacing); } } } # Determine the $sql and @values needed to save this object. if ($table_action eq 'delete') { # A row loaded from the database with its object deleted. # Delete the row in the database. #grab fk_constraints so we can undef non primary-key nullable fks before delete my @non_pk_nullable_fk_columns = $self->get_non_primary_key_nullable_foreign_key_columns_for_table($table); my @values = $self->_id_values_for_primary_key($table,$object_to_save); my $where = $self->_matching_where_clause($table, \@values); if (@non_pk_nullable_fk_columns) { #generate an update statement to set nullable fk columns to null pre delete my $update_sql = "UPDATE "; $update_sql .= "${db_owner}." if ($db_owner); $update_sql .= "$table_name_to_update SET "; $update_sql .= join(", ", map { "$_=?"} @non_pk_nullable_fk_columns); $update_sql .= " WHERE $where"; my @update_values = @values; for (@non_pk_nullable_fk_columns){ unshift @update_values, undef; } my $update_command = { type => 'update', table_name => $table_name, column_names => \@non_pk_nullable_fk_columns, sql => $update_sql, params => \@update_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; push @commands, $update_command; } my $sql = " DELETE FROM "; $sql .= "${db_owner}." if ($db_owner); $sql .= "$table_name_to_update WHERE $where"; push @commands, { type => 'delete', table_name => $table_name, column_names => undef, sql => $sql, params => \@values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; #print Data::Dumper::Dumper \@commands; } elsif ($table_action eq 'update') { # Pre-existing row. # Update in the database if there are columns which have changed. my $changes_for_this_table; if (@save_table_names > 1) { my @changes = map { $_ => $change_summary->{$_} } grep { $class_object->table_for_property($_) eq $table_name } keys %$change_summary; $changes_for_this_table = {@changes}; } else { # Shortcut and use the overall changes summary when # there is only one table. $changes_for_this_table = $change_summary; } my(@changed_cols,@values); for my $property (keys %$changes_for_this_table) { my $column_name = $class_object->column_for_property($property); Carp::croak("No column in table $table_name for property $property?") unless $column_name; push @changed_cols, $column_name; push @values, $changes_for_this_table->{$property}; } if (@changed_cols) { my @changed_values = map { defined ($_) && $object_to_save->can($_) ? $object_to_save->$_ : undef } map { $class_object->property_for_column($_) || undef } @changed_cols; my @id_values = $self->_id_values_for_primary_key($table,$object_to_save); if (scalar(@changed_cols) != scalar(@changed_values)) { no warnings 'uninitialized'; my $mapping = join("\n", map { " $_ => ".$class_object->property_for_column($_) } @changed_cols); Carp::croak("Column count mismatch while updating table $table_name_to_update. " . "The table metadata expects to see ".scalar(@changed_cols) . " columns, but ".scalar(@values)." were retrieved from the object of type " . $object_to_save->class . ".\nCurrent column => property mapping:\n$mapping\n" . "There is probably a mismatch between the database column metadata and the column_name " . "property metadata"); } my @all_values = ( @changed_values, @id_values ); my $where = $self->_matching_where_clause($table, \@all_values); my $sql = " UPDATE "; $sql .= "${db_owner}." if ($db_owner); $sql .= "$table_name_to_update SET " . join(",", map { "$_ = ?" } @changed_cols) . " WHERE $where"; push @commands, { type => 'update', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@all_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } elsif ($table_action eq 'insert') { # An object without a row in the database. # Insert into the database. my @changed_cols = reverse sort map { $class_object->column_for_property($_->property_name) } grep { ! $_->is_transient } grep { ($class_object->table_for_property($_->property_name) || '') eq $table_name } grep { $_->column_name } $class_object->all_property_metas(); my $sql = " INSERT INTO "; $sql .= "${db_owner}." if ($db_owner); $sql .= "$table_name_to_update (" . join(",", @changed_cols) . ") VALUES (" . join(',', split(//,'?' x scalar(@changed_cols))) . ")"; my @values = map { # when there is a column but no property, use NULL as the value defined($_) && $object_to_save->can($_) ? $object_to_save->$_ : undef } map { $class_object->property_for_column($_) || undef } (@changed_cols); if (scalar(@changed_cols) != scalar(@values)) { no warnings 'uninitialized'; my $mapping = join("\n", map { " $_ => ".$class_object->property_for_column($_) } @changed_cols); Carp::croak("Column count mismatch while inserting into table $table_name_to_update. " . "The table metadata expects to see ".scalar(@changed_cols) . " columns, but ".scalar(@values)." were retrieved from the object of type " . $object_to_save->class . ".\nCurrent column => property mapping:\n$mapping\n" . "There is probably a mismatch between the database column metadata and the column_name " . "property metadata"); } #grab fk_constraints so we can undef non primary-key nullable fks before delete my %non_pk_nullable_fk_columns = map { $_ => 1 } $self->get_non_primary_key_nullable_foreign_key_columns_for_table($table); if (%non_pk_nullable_fk_columns){ my @insert_values; my %update_values; for (my $i = 0; $i < @changed_cols; $i++){ my $col = $changed_cols[$i]; if ($non_pk_nullable_fk_columns{$col}) { push @insert_values, undef; $update_values{$col} = $values[$i]; }else{ push @insert_values, $values[$i]; } } push @commands, { type => 'insert', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@insert_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; ##$DB::single = 1; # %update_values can be empty if the Metadb is out of date, and has a fk constraint column # that no longer exists in the class metadata if (%update_values) { my @pk_values = $self->_id_values_for_primary_key($table, $object_to_save); my $where = $self->_matching_where_clause($table, \@pk_values); my @update_cols = keys %update_values; my @update_values = ((map {$update_values{$_}} @update_cols), @pk_values); my $update_sql = " UPDATE "; $update_sql .= "${db_owner}." if ($db_owner); $update_sql .= "$table_name_to_update SET ". join(",", map { "$_ = ?" } @update_cols) . " WHERE $where"; push @commands, { type => 'update', table_name => $table_name, column_names => \@update_cols, sql => $update_sql, params => \@update_values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } else { push @commands, { type => 'insert', table_name => $table_name, column_names => \@changed_cols, sql => $sql, params => \@values, class => $table_class, id => $id, dbh => $data_source->get_default_handle }; } } else { die "Unknown action $table_action for $object_to_save" . Dumper($object_to_save) . "\n"; } } # next table return @commands; } sub _do_on_default_dbh { my $self = shift; my $method = shift; return 1 unless $self->has_default_dbh(); my $dbh = $self->get_default_handle; unless ($dbh->$method(@_)) { $self->error_message("DataSource ".$self->get_name." failed to $method: ".$dbh->errstr); return undef; } return 1; } sub commit { my $self = shift; $self->_do_on_default_dbh('commit', @_); } sub rollback { my $self = shift; $self->_do_on_default_dbh('rollback', @_); } sub disconnect { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } my $rv = $self->_do_on_default_dbh('disconnect', @_); $self->is_connected(0); return $rv; } sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $parent_class_data = $self->SUPER::_generate_class_data_for_loading($class_meta); my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my $order_by_columns; do { my @id_column_names; for my $inheritance_class_name (@class_hierarchy) { my $inheritance_class_object = UR::Object::Type->get($inheritance_class_name); unless ($inheritance_class_object->table_name) { next; } @id_column_names = map { my $t = $inheritance_class_object->table_name; ($t) = ($t =~ /(\S+)\s*$/); $t . '.' . $_ } grep { defined } map { my $p = $inheritance_class_object->property_meta_for_name($_); Carp::croak("No property $_ found for " . $inheritance_class_object->class_name) unless $p; $p->column_name; } map { $_->property_name } grep { $_->column_name } $inheritance_class_object->direct_id_property_metas; last if (@id_column_names); } $order_by_columns = \@id_column_names; }; my @all_table_properties; my @direct_table_properties; my $first_table_name = $class_meta->first_table_name; my $sub_classification_method_name; my ($sub_classification_meta_class_name, $subclassify_by); my @base_joins; my $prev_table_name; my $prev_id_column_name; for my $co ( $class_meta, @{ $parent_class_data->{parent_class_objects} } ) { my $table_name = $co->first_table_name; next unless $table_name; #$first_table_name ||= $co->table_name; $sub_classification_method_name ||= $co->sub_classification_method_name; $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_table_properties, map { [$co, $_, $table_name, 0 ] } sort $sort_sub grep { (defined $_->column_name && $_->column_name ne '') or (defined $_->calculate_sql && $_->calculate_sql ne '') } UR::Object::Property->get( class_name => $co->class_name ); @direct_table_properties = @all_table_properties if $class_meta eq $co; } my @lob_column_names; my @lob_column_positions; my $pos = 0; for my $class_property (@all_table_properties) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; my $data_type = $sql_property->data_type || ''; if ($data_type =~ /LOB$/i) { push @lob_column_names, $sql_property->column_name; push @lob_column_positions, $pos; } $pos++; } my $query_config; my $post_process_results_callback; if (@lob_column_names) { $query_config = $self->_prepare_for_lob; if ($query_config) { my $results_row_arrayref; my @lob_ids; my @lob_values; $post_process_results_callback = sub { $results_row_arrayref = shift; my $dbh = $self->get_default_handle; @lob_ids = @$results_row_arrayref[@lob_column_positions]; @lob_values = $self->_post_process_lob_values($dbh,\@lob_ids); @$results_row_arrayref[@lob_column_positions] = @lob_values; $results_row_arrayref; }; } } my $class_data = { %$parent_class_data, all_table_properties => \@all_table_properties, direct_table_properties => \@direct_table_properties, first_table_name => $first_table_name, sub_classification_method_name => $sub_classification_method_name, sub_classification_meta_class_name => $sub_classification_meta_class_name, subclassify_by => $subclassify_by, base_joins => \@base_joins, order_by_columns => $order_by_columns, lob_column_names => \@lob_column_names, lob_column_positions => \@lob_column_positions, query_config => $query_config, post_process_results_callback => $post_process_results_callback, }; return $class_data; } # We're overriding the method in UR::Object because we support 2 more # event types: connect and query sub validate_subscription { my $self = shift; my $subscription_property = shift; my $retval = $self->SUPER::validate_subscription($subscription_property,@_); return $retval if $retval; unless ( defined($subscription_property) and ( #$subscription_property eq 'connect' #or $subscription_property eq 'query' ) ) { $subscription_property = '(undef)' unless defined ($subscription_property); Carp::croak("Unrecognized subscription aspect '$subscription_property'"); } return 1; } sub _select_clause_for_table_property_data { my $self = shift; my $column_data = $self->_select_clause_columns_for_table_property_data(@_); my $select_clause = join(', ',@$column_data); return $select_clause; } sub _select_clause_columns_for_table_property_data { my $self = shift; my @column_data; for my $class_property (@_) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; $sql_table_name ||= $sql_class->table_name; my ($select_table_name) = ($sql_table_name =~ /(\S+)\s*$/s); # FIXME - maybe a better way would be for these sql-calculated properties, the column_name() # or maybe some other related property name) is actually calculated, so this logic # gets encapsulated in there? if (my $sql_function = $sql_property->calculate_sql) { my @calculate_from = ref($sql_property->calculate_from) eq 'ARRAY' ? @{$sql_property->calculate_from} : ( $sql_property->calculate_from ); foreach my $sql_column_name ( @calculate_from ) { $sql_function =~ s/($sql_column_name)/$sql_table_name\.$1/g; } push(@column_data, $sql_function); } else { push(@column_data, $select_table_name . "." . $sql_property->column_name); } } return \@column_data; } # These seem to be standard for most RDBMSs my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'VARCHAR' => ['Text', undef], 'CHAR' => ['Text', 1], 'CHARACTER' => ['Text', 1], 'XML' => ['Text', undef], 'INTEGER' => ['Integer', undef], 'UNSIGNED INTEGER' => ['Integer', undef], 'SIGNED INTEGER' => ['Integer', undef], 'INT' => ['Integer', undef], 'LONG' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'SMALLINT' => ['Integer', undef], 'FLOAT' => ['Number', undef], 'NUMBER' => ['Number', undef], 'DOUBLE' => ['Number', undef], 'DECIMAL' => ['Number', undef], 'REAL' => ['Number', undef], 'BOOL' => ['Boolean', undef], 'BOOLEAN' => ['Boolean', undef], 'BIT' => ['Boolean', undef], 'DATE' => ['DateTime', undef], 'DATETIME' => ['DateTime', undef], 'TIMESTAMP' => ['DateTime', undef], 'TIME' => ['DateTime', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; my $urtype = $ur_data_type_for_vendor_data_type{uc($type)}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } # Given two properties with different 'is', return a 2-element list of # SQL functions to apply to perform a comparison in the DB. 0th element # gets applied to the left side, 1st element to the right. This implementation # uses printf formats where the %s gets fed an SQL expression like # "table.column" # # SQLite basically treats everything as strings, so needs no conversion. # other DBs will have their own conversions sub cast_for_data_conversion { my($class, $prop_meta1, $prop_meta2) = @_; return ('%s', '%s'); } sub do_after_fork_in_child { my $self = shift->_singleton_object; my $dbhs = $self->_all_dbh_hashref; for my $k (keys %$dbhs) { if ($dbhs->{$k}) { $dbhs->{$k}->{InactiveDestroy} = 1; delete $dbhs->{$k}; } } # reset our state back to being "disconnected" $self->_default_dbh(undef); $self->_all_dbh_hashref({}); $self->is_connected(0); # now force a reconnect $self->get_default_handle(); return 1; } 1; =pod =head1 NAME UR::DataSource::RDBMS - Abstract base class for RDBMS-type data sources =head1 DESCRIPTION This class implements the interface UR uses to query RDBMS databases with DBI. It encapsulates the system's knowledge of classes/properties relation to tables/columns, and how to generate SQL to create, retrieve, update and delete table rows that represent object instances. =head1 SEE ALSO UR::DataSource, UR::DataSource::Oracle, UR::DataSource::Pg, UR::DataSource::SQLite UR::DataSource::MySQL =cut Meta.sqlite3-dump-boostrap000444023532023421 3211212121654174 21754 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank','integer',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','er_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_length','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_ddl_time','timestamp',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','nullable','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_type','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','remarks','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','remarks','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name',6); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name',3); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); COMMIT; Default.pm000444023532023421 757312121654174 16705 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::Default; # NOTE: UR::DataSource::QueryPlan currently has conditional logic for this class use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; class UR::DataSource::Default { is => ['UR::DataSource','UR::Singleton'], doc => 'allows the class to describe its own loading strategy' }; sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $subject_class_name = $rule->subject_class_name; unless ($subject_class_name->can('__load__')) { Carp::croak("Can't load from class $subject_class_name: UR::DataSource::Default requires the class to implement __load__"); } my $template = $rule->template; my ($query_plan) = $self->_resolve_query_plan($template); my $expected_headers = $query_plan->{loading_templates}[0]{property_names}; my ($headers, $content) = $subject_class_name->__load__($rule,$expected_headers); my $iterator; if (ref($content) eq 'ARRAY') { $iterator = sub { my $next_row = shift @$content; $content = undef if @$content == 0; return $next_row; }; } elsif (ref($content) eq 'CODE') { $iterator = $content; } else { Carp::confess("Expected an arrayref of properties, and then content in the form of an arrayref (rows,columns) or coderef/iterator returning rows from $subject_class_name __load__!\n"); } if ("@$headers" ne "@$expected_headers") { # translate the headers into the appropriate order my @mapping = eval { _map_fields($headers,$expected_headers);}; if ($@) { Carp::croak("Loading data for class $subject_class_name and boolexpr $rule failed: $@"); } # print Data::Dumper::Dumper($headers,$expected_headers,\@mapping); my $orig_iterator = $iterator; $iterator = sub { my $result = $orig_iterator->(); return unless $result; my @result2 = @$result[@mapping]; return \@result2; }; } return $iterator; } sub can_savepoint { 0 } sub _map_fields { my ($from,$to) = @_; my $n = 0; my %from = map { $_ => $n++ } @$from; my @pos; for my $field (@$to) { my $pos = $from{$field}; unless (defined $pos) { #print "@$from\n@$to\n" . Carp::longmess() . "\n"; die("Can't resolve value for '$field' from the headers returned by its __load__: ". join(', ', @$from)); } push @pos, $pos; } return @pos; } # Nothing to be done for commit and rollback sub rollback { 1;} sub commit { 1; } sub _sync_database { my $self = shift; my %params = @_; my $changed_objects = $params{changed_objects}; my %class_can_save; my @saved; eval { for my $obj (@$changed_objects) { my $obj_class = $obj->class; unless (exists $class_can_save{$obj_class}) { $class_can_save{$obj_class} = $obj->can('__save__'); } if ($class_can_save{$obj_class}) { push @saved, $obj; $obj->__save__; } } }; if ($@) { my $err = $@; my @failed_rollback; while (my $obj = shift @saved) { eval { $obj->__rollback__; }; if ($@) { push @failed_rollback, $obj; } } if (@failed_rollback) { print Data::Dumper::Dumper("Failed Rollback:", \@failed_rollback); die "Failed to save, and ERRORS DURING ROLLBACK:\n$err\n $@\n"; } die $@; } my @failed_commit; unless ($@) { # all saves worked, commit while (my $obj = shift @saved) { eval { $obj->__commit__; }; if ($@) { push @failed_commit, $@ => $obj; } }; } return 1; } 1; CSV.pm000444023532023421 2320112121654174 15756 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::CSV; use strict; use warnings; # There are still a few issues with actually using this thing: # # when running # ur define datasource rdbms --dsn DBD:CSV:f_dir=/tmp/trycsv # the f_dir=... part doesn't get put in as a "server" attribute. # You have to add in it by hand as sub server {} # # after ur update classes, there aren't any id properties defined # for your new classes, because there's no conclusive way to pick # the right one - no unique constraints # # _get_sequence_name_for_table_and_column() and _get_next_value_from_sequence() # aren't implemented yet, so creating new entities and sync_databases # won't work # # There's a bug in even the latest SQL::Statement on CPAN where the processing # of JOIN clauses uses a case sensitive match against upper-case stuff, when it # should be lower-case. It also cannot handle more than one join in the same # statement # # with that out of the way... on to the show! require UR; our $VERSION = "0.41"; # UR $VERSION; use File::Basename; UR::Object::Type->define( class_name => 'UR::DataSource::CSV', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "CSV" } sub owner { undef } sub login { undef } sub auth { undef } sub path { my $server = shift->server; my @server_opts = split(';', $server); foreach my $opt ( @server_opts ) { my($key,$value) = split('=',$opt); if ($key eq 'f_dir') { return $value; } } return; } sub can_savepoint { 0;} # Dosen't support savepoints sub _dbi_connect_args { my $self = shift; my @connection = $self->SUPER::_dbi_connect_args(@_); delete $connection[3]->{'AutoCommit'}; # DBD::CSV doesn't support autocommit being off return @connection; } sub _get_sequence_name_for_table_and_column { Carp::croak("Not implemented yet"); my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_seq', $table_name, $column_name); unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTOINCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { Carp::croak('Not implemented yet'); my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } # Given a table name, return the complete pathname to it # As ur update classes calls this, $table has been uppercased # already (because most data sources uppercase table names), # so we need to figure out which file they're talking about sub _find_pathname_for_table { my $self = shift; my $table = shift; my $path = $self->path; my @all_files = glob("$path/*"); # note: this only finds the first one foreach my $pathname ( @all_files ) { if (File::Basename::basename($pathname) eq $table) { return $pathname; } } return; } # column_info doesn't work against a DBD::CSV handle sub get_column_details_from_data_dictionary { my($self,$catalog,$schema,$table,$column) = @_; # Convert the SQL wildcards to glob wildcards $table =~ tr/%_/*?/; # Convert the SQL wildcards to regex wildcards $column =~ s/%/\\w*/; $column =~ s/_/\\w/; my $column_regex = qr($column); my(@matching_files) = $self->_find_pathname_for_table($table); my @found_columns; foreach my $file ( @matching_files ) { my $table_name = File::Basename::basename($file); my $fh = IO::File->new($file); unless ($fh) { $self->warning_message("Can't open file $file for reading: $!"); next; } my $header = $fh->getline(); $header =~ s/\r|\n//g; # Remove newline/CR my @columns = split($self->get_default_handle->{'csv_sep_char'} ||',' , $header); my $column_order = 0; foreach my $column_name ( @columns ) { $column_order++; next unless $column_name =~ m/$column_regex/; push @found_columns, { TABLE_CAT => $catalog, TABLE_SCHEM => $schema, TABLE_NAME => $table_name, COLUMN_NAME => $column_name, DATA_TYPE => 'STRING', # what else could we put here? TYPE_NAME => 'STRING', NULLABLE => 1, # all columns are nullable in CSV files IS_NULLABLE => 'YES', REMARKS => '', COLUMN_DEF => '', SQL_DATA_TYPE => '', # FIXME shouldn't this be something related to DATA_TYPE SQL_DATETIME_SUB => '', CHAH_OCTET_LENGTH => undef, # FIXME this should be the same as column_size, right? ORDINAL_POSITION => $column_order, } } } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE ); my $returned_sth = $sponge->prepare("column_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @found_columns ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # DBD::CSV doesn't support foreign key tracking # returns a statement handle with no data to read sub get_foreign_key_details_from_data_dictionary { my($self,$fk_catalog,$fk_schema,$fk_table,$pk_catalog,$pk_schema,$pk_table) = @_; my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( FK_NAME UK_TABLE_NAME UK_COLUMN_NAME FK_TABLE_NAME FK_COLUMN_NAME ); my $table = $pk_table || $fk_table; my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # DBD::CSV dosen't support bitmap indicies, so there aren't any sub get_bitmap_index_details_from_data_dictionary { return []; } # DBD::CSV doesn't support unique constraints sub get_unique_index_details_from_data_dictionary { return {}; } sub get_table_details_from_data_dictionary { my($self,$catalog,$schema,$table,$type) = @_; # DBD::CSV's table_info seems to always give you back all the "tables" even # if you only asked for details on one of them my $sth = $self->SUPER::get_table_details_from_data_dictionary($catalog,$schema,$table,$type); # Yeah, it's kind of silly to have to read in all the data and repackage it # back into another sth my @returned_details; while (my $row = $sth->fetchrow_arrayref()) { next unless ($row->[2] eq $table); push @returned_details, $row; } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $self->get_default_handle->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS ); my $returned_sth = $sponge->prepare("table_info $table", { rows => \@returned_details, NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $self->get_default_handle->DBI::set_err($sponge->err(), $sponge->errstr()); $returned_sth; } # By default, make a text dump of the database at commit time. # This should really be a datasource property sub dump_on_commit { 0; } 1; =pod =head1 NAME UR::DataSource::CSV - Parent class for data sources using DBD::CSV =head1 DESCRIPTION UR::DataSource::CSV is a subclass of L and can be used for interacting with CSV files. Because of the limitations of the underlying modules (such as SQL::Statement only supporting one join at a time), this module is deprecated. L implements a non-SQL interface for data files, and is the proper way to use a file as a data source for class data. =cut Oracle.pm000444023532023421 5303312121654174 16536 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::Oracle; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Oracle', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); sub driver { "Oracle" } sub owner { shift->_singleton_object->login } sub can_savepoint { 1 } # Oracle supports savepoints inside transactions sub does_support_recursive_queries { 'connect by' }; sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("savepoint $sp_name"); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("rollback to $sp_name"); } my $DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'; my $TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SSXFF'; sub _init_created_dbh { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; $dbh->do("alter session set NLS_DATE_FORMAT = '$DATE_FORMAT'"); $dbh->do("alter session set NLS_TIMESTAMP_FORMAT = '$TIMESTAMP_FORMAT'"); return $dbh; } sub _dbi_connect_args { my @args = shift->SUPER::_dbi_connect_args(@_); $args[3]{ora_module_name} = (UR::Context::Process->get_current->prog_name || $0); return @args; } sub _prepare_for_lob { { ora_auto_lob => 0 } } sub _post_process_lob_values { my ($self, $dbh, $lob_id_arrayref) = @_; return map { if (defined($_)) { my $length = $dbh->ora_lob_length($_); my $data = $dbh->ora_lob_read($_, 1, $length); # TODO: bind to a file for items of a certain size to save RAM. # Special work with tying a scalar to the file? $data; } else { undef; } } @$lob_id_arrayref; } sub _value_is_null { my ($class,$value) = @_; return 1 if not defined $value; return 1 if $value eq ''; return 1 if (ref($value) eq 'HASH' and $value->{operator} eq '=' and (!defied($value->{value}) or $value->{value} eq '')); return 0; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /\$/; } sub get_table_last_ddl_times_by_table_name { my $self = shift; my $sql = qq| select object_name table_name, last_ddl_time from all_objects o where o.owner = ? and (o.object_type = 'TABLE' or o.object_type = 'VIEW') |; my $data = $self->get_default_handle->selectall_arrayref( $sql, undef, $self->owner ); return { map { @$_ } @$data }; }; sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; # we may need to change how this db handle is gotten my $dbh = $self->get_default_handle; my $new_id = $dbh->selectrow_array("SELECT " . $sequence_name . ".nextval from DUAL"); if ($dbh->err) { die "Failed to prepare SQL to generate a column id from sequence: $sequence_name.\n" . $dbh->errstr . "\n"; return; } return $new_id; } sub get_bitmap_index_details_from_data_dictionary { my($self,$table_name) = @_; my $sql = qq( select c.table_name,c.column_name,c.index_name from all_indexes i join all_ind_columns c on i.index_name = c.index_name where i.index_type = 'BITMAP' ); my @select_params; if ($table_name) { @select_params = $self->_resolve_owner_and_table_from_table_name($table_name); $sql .= " and i.table_owner = ? and i.table_name = ?"; } my $dbh = $self->get_default_handle; my $rows = $dbh->selectall_arrayref($sql, undef, @select_params); return undef unless $rows; my @ret = map { { table_name => $_->[0], column_name => $_->[1], index_name => $_->[2] } } @$rows; return \@ret; } sub get_unique_index_details_from_data_dictionary { my ($self,$table_name) = @_; my $sql = qq( select cc.constraint_name, cc.column_name from all_cons_columns cc join all_constraints c on c.constraint_name = cc.constraint_name and c.owner = cc.owner and c.constraint_type = 'U' where cc.table_name = ? and cc.owner = ? union select ai.index_name, aic.column_name from all_indexes ai join all_ind_columns aic on aic.index_name = ai.index_name and aic.index_owner = ai.owner where ai.uniqueness = 'UNIQUE' and aic.table_name = ? and aic.index_owner = ? ); my $dbh = $self->get_default_handle(); return undef unless $dbh; my $sth = $dbh->prepare($sql); return undef unless $sth; my($db_owner,$dd_table_name) = $self->_resolve_owner_and_table_from_table_name($table_name); $sth->execute($table_name, $db_owner, $dd_table_name, $db_owner); my $ret; while (my $data = $sth->fetchrow_hashref()) { $ret->{$data->{'CONSTRAINT_NAME'}} ||= []; push @{ $ret->{ $data->{CONSTRAINT_NAME} } }, $data->{COLUMN_NAME}; } return $ret; } sub set_userenv { # there are two places to set these oracle variables- # 1. this method in UR::DataSource::Oracle is a class method # that can be called to change the values later # 2. the method in YourSubclass::DataSource::Oracle is called in # _init_created_dbh which is called while the datasource # is still being set up- it operates directly on the db handle my ($self, %p) = @_; my $dbh = $p{'dbh'} || $self->get_default_handle(); # module is application name my $module = $p{'module'} || $0; # storing username in 'action' oracle variable my $action = $p{'action'}; if (! defined($action)) { $action = getpwuid($>); # real UID } my $sql = q{BEGIN dbms_application_info.set_module(?, ?); END;}; my $sth = $dbh->prepare($sql); if (!$sth) { warn "Couldnt prepare query to set module/action in Oracle"; return undef; } $sth->execute($module, $action) || warn "Couldnt set module/action in Oracle"; } sub get_userenv { # there are two ways to set these values but this is # the only way to retreive the values after they are set my ($self, $dbh) = @_; if (!$dbh) { $dbh = $self->get_default_handle(); } if (!$dbh) { warn "No dbh"; return undef; } my $sql = q{ SELECT sys_context('USERENV','MODULE') as module, sys_context('USERENV','ACTION') as action FROM dual }; my $sth = $dbh->prepare($sql); return undef unless $sth; $sth->execute() || die "execute failed: $!"; my $r = $sth->fetchrow_hashref(); return $r; } my %ur_data_type_for_vendor_data_type = ( 'VARCHAR2' => ['Text', undef], 'BLOB' => ['XmlBlob', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; my $urtype = $ur_data_type_for_vendor_data_type{uc($type)}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; for (my $n = 0; $n < @$column_objects; $n++) { next unless defined ($column_objects->[$n]); # No metaDB info for this one if ($column_objects->[$n]->data_type eq 'BLOB') { $sth->bind_param($n+1, undef, { ora_type => 23 }); } } } sub get_connection_debug_info { my $self = shift; my @debug_info = $self->SUPER::get_connection_debug_info(@_); push @debug_info, ( "DBD::Oracle Version: ", $DBD::Oracle::VERSION, "\n", "TNS_ADMIN: ", $ENV{TNS_ADMIN}, "\n", "ORACLE_HOME: ", $ENV{ORACLE_HOME}, "\n", ); return @debug_info; } # This is a near cut-and-paste from DBD::Oracle, with the exception that # the query hint is removed, since it performs poorly on Oracle 11 sub get_table_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_table_details_from_data_dictionary(@_); } my($CatVal, $SchVal, $TblVal, $TypVal) = @_; my $dbh = $self->get_default_handle(); # XXX add knowledge of temp tables, etc # SQL/CLI (ISO/IEC JTC 1/SC 32 N 0595), 6.63 Tables if (ref $CatVal eq 'HASH') { ($CatVal, $SchVal, $TblVal, $TypVal) = @$CatVal{'TABLE_CAT','TABLE_SCHEM','TABLE_NAME','TABLE_TYPE'}; } my @Where = (); my $SQL; if ( defined $CatVal && $CatVal eq '%' && (!defined $SchVal || $SchVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19a $SQL = <<'SQL'; SELECT NULL TABLE_CAT , NULL TABLE_SCHEM , NULL TABLE_NAME , NULL TABLE_TYPE , NULL REMARKS FROM DUAL SQL } elsif ( defined $SchVal && $SchVal eq '%' && (!defined $CatVal || $CatVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19b $SQL = <<'SQL'; SELECT NULL TABLE_CAT , s TABLE_SCHEM , NULL TABLE_NAME , NULL TABLE_TYPE , NULL REMARKS FROM ( SELECT USERNAME s FROM ALL_USERS UNION SELECT 'PUBLIC' s FROM DUAL ) ORDER BY TABLE_SCHEM SQL } elsif ( defined $TypVal && $TypVal eq '%' && (!defined $CatVal || $CatVal eq '') && (!defined $SchVal || $SchVal eq '') && (!defined $TblVal || $TblVal eq '')) { # Rule 19c $SQL = <<'SQL'; SELECT NULL TABLE_CAT , NULL TABLE_SCHEM , NULL TABLE_NAME , t.tt TABLE_TYPE , NULL REMARKS FROM ( SELECT 'TABLE' tt FROM DUAL UNION SELECT 'VIEW' tt FROM DUAL UNION SELECT 'SYNONYM' tt FROM DUAL UNION SELECT 'SEQUENCE' tt FROM DUAL ) t ORDER BY TABLE_TYPE SQL } else { $SQL = <<'SQL'; SELECT * FROM ( SELECT NULL TABLE_CAT , t.OWNER TABLE_SCHEM , t.TABLE_NAME TABLE_NAME , decode(t.OWNER , 'SYS' , 'SYSTEM ' , 'SYSTEM' , 'SYSTEM ' , '' ) || t.TABLE_TYPE TABLE_TYPE , c.COMMENTS REMARKS FROM ALL_TAB_COMMENTS c , ALL_CATALOG t WHERE c.OWNER (+) = t.OWNER AND c.TABLE_NAME (+) = t.TABLE_NAME AND c.TABLE_TYPE (+) = t.TABLE_TYPE ) SQL if ( defined $SchVal ) { push @Where, "TABLE_SCHEM LIKE '$SchVal' ESCAPE '\\'"; } if ( defined $TblVal ) { push @Where, "TABLE_NAME LIKE '$TblVal' ESCAPE '\\'"; } if ( defined $TypVal ) { my $table_type_list; $TypVal =~ s/^\s+//; $TypVal =~ s/\s+$//; my @ttype_list = split (/\s*,\s*/, $TypVal); foreach my $table_type (@ttype_list) { if ($table_type !~ /^'.*'$/) { $table_type = "'" . $table_type . "'"; } $table_type_list = join(", ", @ttype_list); } push @Where, "TABLE_TYPE IN ($table_type_list)"; } $SQL .= ' WHERE ' . join("\n AND ", @Where ) . "\n" if @Where; $SQL .= " ORDER BY TABLE_TYPE, TABLE_SCHEM, TABLE_NAME\n"; } my $sth = $dbh->prepare($SQL) or return undef; $sth->execute or return undef; $sth; } sub get_column_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_column_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my $attr = ( ref $_[0] eq 'HASH') ? $_[0] : { 'TABLE_SCHEM' => $_[1],'TABLE_NAME' => $_[2],'COLUMN_NAME' => $_[3] }; my($typecase,$typecaseend) = ('',''); my $v = DBD::Oracle::db::ora_server_version($dbh); if (!defined($v) or $v->[0] >= 8) { $typecase = <<'SQL'; CASE WHEN tc.DATA_TYPE LIKE 'TIMESTAMP% WITH% TIME ZONE' THEN 95 WHEN tc.DATA_TYPE LIKE 'TIMESTAMP%' THEN 93 WHEN tc.DATA_TYPE LIKE 'INTERVAL DAY% TO SECOND%' THEN 110 WHEN tc.DATA_TYPE LIKE 'INTERVAL YEAR% TO MONTH' THEN 107 ELSE SQL $typecaseend = 'END'; } my $SQL = <<"SQL"; SELECT * FROM ( SELECT to_char( NULL ) TABLE_CAT , tc.OWNER TABLE_SCHEM , tc.TABLE_NAME TABLE_NAME , tc.COLUMN_NAME COLUMN_NAME , $typecase decode( tc.DATA_TYPE , 'MLSLABEL' , -9106 , 'ROWID' , -9104 , 'UROWID' , -9104 , 'BFILE' , -4 -- 31? , 'LONG RAW' , -4 , 'RAW' , -3 , 'LONG' , -1 , 'UNDEFINED', 0 , 'CHAR' , 1 , 'NCHAR' , 1 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 8, 3 ) , 'FLOAT' , 8 , 'VARCHAR2' , 12 , 'NVARCHAR2', 12 , 'BLOB' , 30 , 'CLOB' , 40 , 'NCLOB' , 40 , 'DATE' , 93 , NULL ) $typecaseend DATA_TYPE -- ... , tc.DATA_TYPE TYPE_NAME -- std.? , decode( tc.DATA_TYPE , 'LONG RAW' , 2147483647 , 'LONG' , 2147483647 , 'CLOB' , 2147483647 , 'NCLOB' , 2147483647 , 'BLOB' , 2147483647 , 'BFILE' , 2147483647 , 'NUMBER' , decode( tc.DATA_SCALE , NULL, 126 , nvl( tc.DATA_PRECISION, 38 ) ) , 'FLOAT' , tc.DATA_PRECISION , 'DATE' , 19 , tc.DATA_LENGTH ) COLUMN_SIZE , decode( tc.DATA_TYPE , 'LONG RAW' , 2147483647 , 'LONG' , 2147483647 , 'CLOB' , 2147483647 , 'NCLOB' , 2147483647 , 'BLOB' , 2147483647 , 'BFILE' , 2147483647 , 'NUMBER' , nvl( tc.DATA_PRECISION, 38 ) + 2 , 'FLOAT' , 8 -- ? , 'DATE' , 16 , tc.DATA_LENGTH ) BUFFER_LENGTH , decode( tc.DATA_TYPE , 'DATE' , 0 , tc.DATA_SCALE ) DECIMAL_DIGITS -- ... , decode( tc.DATA_TYPE , 'FLOAT' , 2 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 2, 10 ) , NULL ) NUM_PREC_RADIX , decode( tc.NULLABLE , 'Y' , 1 , 'N' , 0 , NULL ) NULLABLE , cc.COMMENTS REMARKS , tc.DATA_DEFAULT COLUMN_DEF -- Column is LONG! , decode( tc.DATA_TYPE , 'MLSLABEL' , -9106 , 'ROWID' , -9104 , 'UROWID' , -9104 , 'BFILE' , -4 -- 31? , 'LONG RAW' , -4 , 'RAW' , -3 , 'LONG' , -1 , 'UNDEFINED', 0 , 'CHAR' , 1 , 'NCHAR' , 1 , 'NUMBER' , decode( tc.DATA_SCALE, NULL, 8, 3 ) , 'FLOAT' , 8 , 'VARCHAR2' , 12 , 'NVARCHAR2', 12 , 'BLOB' , 30 , 'CLOB' , 40 , 'NCLOB' , 40 , 'DATE' , 9 -- not 93! , NULL ) SQL_DATA_TYPE -- ... , decode( tc.DATA_TYPE , 'DATE' , 3 , NULL ) SQL_DATETIME_SUB -- ... , to_number( NULL ) CHAR_OCTET_LENGTH -- TODO , tc.COLUMN_ID ORDINAL_POSITION , decode( tc.NULLABLE , 'Y' , 'YES' , 'N' , 'NO' , NULL ) IS_NULLABLE FROM ALL_TAB_COLUMNS tc , ALL_COL_COMMENTS cc WHERE tc.OWNER = cc.OWNER AND tc.TABLE_NAME = cc.TABLE_NAME AND tc.COLUMN_NAME = cc.COLUMN_NAME ) WHERE 1 = 1 SQL my @BindVals = (); while ( my ( $k, $v ) = each %$attr ) { if ( $v ) { $SQL .= " AND $k LIKE ? ESCAPE '\\'\n"; push @BindVals, $v; } } $SQL .= " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION\n"; my $sth = $dbh->prepare( $SQL ) or return undef; $sth->execute( @BindVals ) or return undef; $sth; } sub get_primary_key_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_primary_key_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my($catalog, $schema, $table) = @_; if (ref $catalog eq 'HASH') { ($schema, $table) = @$catalog{'TABLE_SCHEM','TABLE_NAME'}; $catalog = undef; } my $SQL = <<'SQL'; SELECT * FROM ( SELECT NULL TABLE_CAT , c.OWNER TABLE_SCHEM , c.TABLE_NAME TABLE_NAME , c.COLUMN_NAME COLUMN_NAME , c.POSITION KEY_SEQ , c.CONSTRAINT_NAME PK_NAME FROM ALL_CONSTRAINTS p , ALL_CONS_COLUMNS c WHERE p.OWNER = c.OWNER AND p.TABLE_NAME = c.TABLE_NAME AND p.CONSTRAINT_NAME = c.CONSTRAINT_NAME AND p.CONSTRAINT_TYPE = 'P' ) WHERE TABLE_SCHEM = ? AND TABLE_NAME = ? ORDER BY TABLE_SCHEM, TABLE_NAME, KEY_SEQ SQL #warn "@_\n$Sql ($schema, $table)"; my $sth = $dbh->prepare($SQL) or return undef; $sth->execute($schema, $table) or return undef; $sth; } sub get_foreign_key_details_from_data_dictionary { my $self = shift; my $version = $self->_get_oracle_major_server_version(); if ($version < '11') { return $self->SUPER::get_foreign_key_details_from_data_dictionary(@_); } my $dbh = $self->get_default_handle(); my $attr = ( ref $_[0] eq 'HASH') ? $_[0] : { 'UK_TABLE_SCHEM' => $_[1],'UK_TABLE_NAME ' => $_[2] ,'FK_TABLE_SCHEM' => $_[4],'FK_TABLE_NAME ' => $_[5] }; my $SQL = <<'SQL'; # XXX: DEFERABILITY SELECT * FROM ( SELECT to_char( NULL ) UK_TABLE_CAT , uk.OWNER UK_TABLE_SCHEM , uk.TABLE_NAME UK_TABLE_NAME , uc.COLUMN_NAME UK_COLUMN_NAME , to_char( NULL ) FK_TABLE_CAT , fk.OWNER FK_TABLE_SCHEM , fk.TABLE_NAME FK_TABLE_NAME , fc.COLUMN_NAME FK_COLUMN_NAME , uc.POSITION ORDINAL_POSITION , 3 UPDATE_RULE , decode( fk.DELETE_RULE, 'CASCADE', 0, 'RESTRICT', 1, 'SET NULL', 2, 'NO ACTION', 3, 'SET DEFAULT', 4 ) DELETE_RULE , fk.CONSTRAINT_NAME FK_NAME , uk.CONSTRAINT_NAME UK_NAME , to_char( NULL ) DEFERABILITY , decode( uk.CONSTRAINT_TYPE, 'P', 'PRIMARY', 'U', 'UNIQUE') UNIQUE_OR_PRIMARY FROM ALL_CONSTRAINTS uk , ALL_CONS_COLUMNS uc , ALL_CONSTRAINTS fk , ALL_CONS_COLUMNS fc WHERE uk.OWNER = uc.OWNER AND uk.CONSTRAINT_NAME = uc.CONSTRAINT_NAME AND fk.OWNER = fc.OWNER AND fk.CONSTRAINT_NAME = fc.CONSTRAINT_NAME AND uk.CONSTRAINT_TYPE IN ('P','U') AND fk.CONSTRAINT_TYPE = 'R' AND uk.CONSTRAINT_NAME = fk.R_CONSTRAINT_NAME AND uk.OWNER = fk.R_OWNER AND uc.POSITION = fc.POSITION ) WHERE 1 = 1 SQL my @BindVals = (); while ( my ( $k, $v ) = each %$attr ) { if ( $v ) { $SQL .= " AND $k = ?\n"; push @BindVals, $v; } } $SQL .= " ORDER BY UK_TABLE_SCHEM, UK_TABLE_NAME, FK_TABLE_SCHEM, FK_TABLE_NAME, ORDINAL_POSITION\n"; my $sth = $dbh->prepare( $SQL ) or return undef; $sth->execute( @BindVals ) or return undef; $sth; } sub _get_oracle_major_server_version { my $self = shift; unless (exists $self->{'__ora_major_server_version'}) { my $dbh = $self->get_default_handle(); my @data = $dbh->selectrow_arrayref('select version from v$instance'); $self->{'__ora_major_server_version'} = (split(/\./, $data[0]->[0]))[0]; } return $self->{'__ora_major_server_version'}; } sub cast_for_data_conversion { my($class, $prop_meta1, $prop_meta2) = @_; my @retval = ('%s','%s'); my $prop_meta1_type = $prop_meta1->_data_type_as_class_name; my $prop_meta2_type = $prop_meta2->_data_type_as_class_name; #printf("Cast %s::%s (%s) and %s::%s (%s)\n", # $prop_meta1->class_name, $prop_meta1->property_name, $prop_meta1_type, # $prop_meta2->class_name, $prop_meta2->property_name, $prop_meta2_type); if ($prop_meta1_type->isa($prop_meta2_type) or $prop_meta2_type->isa($prop_meta1_type) ) { return @retval; } if (! $prop_meta1_type->isa('UR::Value::Text') and ! $prop_meta2_type->isa('UR::Value::Text') ) { # We only support cases where one is a string, for now # hopefully the DB can sort it out return @retval; } # Figure out which one is the non-string my($data_type, $i) = $prop_meta1_type->isa('UR::Value::Text') ? ( $prop_meta2_type, 1) : ( $prop_meta1_type, 0); if ($data_type->isa('UR::Value::Number')) { $retval[$i] = q{to_char(%s)}; } elsif ($data_type->isa('UR::Value::Timestamp')) { # These time formats shoule match what's given in _init_created_dbh $retval[$i] = qq{to_char(%s, '$TIMESTAMP_FORMAT')}; } elsif ($data_type->isa('UR::Value::DateTime')) { $retval[$i] = qq{to_char(%s, '$DATE_FORMAT')}; } else { @retval = $class->SUPER::cast_for_data_conversion($prop_meta1, $prop_meta2); } return @retval; } 1; =pod =head1 NAME UR::DataSource::Oracle - Oracle specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the Oracle-specific methods necessary for interacting with Oracle databases =head1 SEE ALSO L, L =cut Code.pm000444023532023421 265712121654174 16171 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource package UR::DataSource::Code; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; use File::Copy qw//; ##- use UR; UR::Object::Type->define( class_name => 'UR::DataSource::Code', is => ['UR::DataSource::SQLite'], ); sub server { my $self = shift->_singleton_object(); my $path = $self->__meta__->module_path; $path =~ s/\.pm$/.db/ or Carp::confess("Bad module path for resolving server!"); unless (-e $path) { # initialize a new database from the one in the base class # should this be moved to connect time? my $template_database_file = UR::DataSource::Code->server(); if ($self->class eq __PACKAGE__) { Carp::confess("Missing template database file: $path!"); } unless (-e $template_database_file) { Carp::confess("Missing template database file: $path! Cannot initialize database for " . $self->class); } unless(File::Copy::copy($template_database_file,$path)) { Carp::confess("Error copying $path to $template_database_file to initialize database!"); } unless(-e $path) { Carp::confess("File $path not found after copy from $template_database_file. Cannot initialize database!"); } } return $path; } sub resolve_class_name_for_table_name_fixups { my $self = shift->_singleton_object; print "fixup @_"; return $self->class . "::", @_; } 1; Meta.sqlite3-dump000444023532023421 3211212121654175 20126 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_table','TABLE','entity',NULL,'2007-04-16 19:35:07',NULL); INSERT INTO "dd_table" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','TABLE','entity',NULL,'2007-04-16 19:35:06',NULL); CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank','integer',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','er_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_length','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','last_ddl_time','timestamp',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','nullable','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_type','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','remarks','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','table_type','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','remarks','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','r_column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name','varchar',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner','varchar',NULL,'Y','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source','varchar',NULL,'N','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner','varchar',NULL,'Y','2007-04-16 19:35:06',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','last_object_revision','timestamp',NULL,'N','2007-04-16 19:35:07',''); INSERT INTO "dd_table_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name','varchar',NULL,'N','2007-04-16 19:35:07',''); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','table_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_owner',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','column_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','rank',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','fk_constraint_name',6); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','fk_constraint_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_unique_constraint_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_pk_constraint_column','column_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_bitmap_index','bitmap_index_name',4); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','table_name',3); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table_column','data_source',1); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_table','owner',2); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint','r_table_name',5); INSERT INTO "dd_pk_constraint_column" VALUES('UR::DataSource::Meta','main','dd_fk_constraint_column','table_name',3); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); COMMIT; Meta.sqlite3000444023532023421 10200012121654175 17175 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceSQLite format 3@ !=Q+indexsqlite_autoindex_dd_table_column_1dd_table_column ++Qtabledd_bitmap_indexdd_bitmap_indexCREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) )=Q+indexsqlite_autoindex_dd_bitmap_index_1dd_bitmap_indexctabledd_tabledd_tableCREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) )/Cindexsqlite_autoindex_dd_table  `_Z 5C3UR::DataSource::Metamaindd_unique_constraint_columnTABLEentity2007-04-16 19:35:06G 53UR::DataSource::Metamaindd_tableTABLEentity2007-04-16 19:35:07V 5;3UR::DataSource::Metamaindd_fk_constraint_columnTABLEentity2007-04-16 19:35:07O 5-3UR::DataSource::Metamaindd_fk_constraintTABLEentity2007-04-16 19:35:07V 5;3UR::DataSource::Metamaindd_pk_constraint_columnTABLEentity2007-04-16 19:35:07N 5+3UR::DataSource::Metamaindd_bitmap_indexTABLEentity2007-04-16 19:35:06N 5+3UR::DataSource::Metamaindd_table_columnTABLEentity2007-04-16 19:35:06 ? n95CUR::DataSource::Metamaindd_unique_constraint_column&5UR::DataSource::Metamaindd_table55;UR::DataSource::Metamaindd_fk_constraint_column.5-UR::DataSource::Metamaindd_fk_constraint55;UR::DataSource::Metamaindd_pk_constraint_column-5+UR::DataSource::Metamaindd_bitmap_index-5+UR::DataSource::Metamaindd_table_column  +mc 5C#3 UR::DataSource::Metamaindd_unique_constraint_columncolumn_namevarcharN2007-04-16 19:35:06W 5+#3 UR::DataSource::Metamaindd_table_columndata_lengthvarcharY2007-04-16 19:35:06g 5C+3 UR::DataSource::Metamaindd_unique_constraint_columnconstraint_namevarcharN2007-04-16 19:35:06L 53 UR::DataSource::Metamaindd_tableer_typevarcharN2007-04-16 19:35:07Y 5;3 UR::DataSource::Metamaindd_fk_constraint_columnownervarcharN2007-04-16 19:35:07X 5;3 UR::DataSource::Metamaindd_pk_constraint_columnrankintegerN2007-04-16 19:35:07Y 5;3 UR::DataSource::Metamaindd_pk_constraint_columnownervarcharY2007-04-16 19:35:07W 5+#3 UR::DataSource::Metamaindd_bitmap_indexdata_sourcevarcharN2007-04-16 19:35:06V 5+!3 UR::DataSource::Metamaindd_bitmap_indextable_namevarcharN2007-04-16 19:35:06[ 553 UR::DataSource::Metamaindd_tablelast_object_revisiontimestamp)  {{dok!%A5-1UR::DataSource::Metamaindd_fk_constraintfk_constraint_name45'UR::DataSource::Metamaindd_tablelast_ddl_time@5;!UR::DataSource::Metamaindd_fk_constraint_columntable_name?5+/UR::DataSource::Metamaindd_bitmap_indexbitmap_index_name B5+5UR::DataSource::Metamaindd_table_columnlast_object_revision @5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name E5C#UR::DataSource::Metamaindd_unique_constraint_columncolumn_name 95+#UR::DataSource::Metamaindd_table_columndata_length I5C+UR::DataSource::Metamaindd_unique_constraint_columnconstraint_name.5UR::DataSource::Metamaindd_tableer_type;5;UR::DataSource::Metamaindd_fk_constraint_columnowner:5;UR::DataSource::Metamaindd_pk_constraint_columnrank;5;UR::DataSource::Metamaindd_pk_constraint_columnowner95+#UR::DataSource::Metamaindd_bitmap 95+#UR::DataSource::Metamaindd_table_columndata_length B5;%UR::DataSource::Metamaindd_fk_constraint_columnr_table_name =Q+indexsqlite_autoindex_dd_table_column_1dd_table_column ctabledd_tabledd_tableCREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) )/Cindexsqlite_autoindex_dd_table_1dd_table ++Qtabledd_bitmap_indexdd_bitmap_indexCREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) )=Q+indexsqlite_autoindex_dd_bitmap_index_1dd_bitmap_index 9x++ktabledd_table_columndd_table_columnCREATE TABLE dd_table_column ( data_sourc++ktabledd_table_columndd_table_columnCREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) )=Q+indexsqlite_autoindex_dd_table_column_1dd_table_column6;;tabledd_pk_constraint_columndd_pk_constraint_columnCREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) )Ma;indexsqlite_autoindex_dd_pk_constraint_column_1dd_pk_constraint_column mK=+mc 5C#3 UR::DataSource::Metamaindd_unique_constraint_columncolumn_namevarcharN2007-04-16 19:35:06W 5+#3 UR::DataSource::Metamaindd_table_columndata_lengthvarcharY2007-04-16 19:35:06g 5C+3 UR::DataSource::Metamaindd_unique_constraint_columnconstraint_namevarcharN2007-04-16 19:35:06L 53 UR::DataSource::Metamaindd_tableer_typevarcharN2007-04-16 19:35:07Y 5;3 UR::DataSource::Metamaindd_fk_constraint_columnownervarcharN2007-04-16 19:35:07X 5;3 UR::DataSource::Metamaindd_pk_constraint_columnrankintegerN2007-04-16 19:35:07Y 5;3 UR::DataSource::Metamaindd_pk_constraint_columnownervarcharY2007-04-16 19:35:07W 5+#3 UR::DataSource::Metamaindd_bitmap_indexdata_sourcevarcharN2007-04-16 19:35:06V 5+!3 UR::DataSource::Metamaindd_bitmap_indextable_namevarcharN2007-04-16 19:35:06[ 553 UR::DataSource::Metamaindd_tablelast_object_revisiontimestampN2007-04-16 19:35:07 g<}'pgV 5+!3 UR::DataSource::Metamaindd_table_columntable_namevarcharN2007-04-16 19:35:06U 5+3 UR::DataSource::Metamaindd_table_columndata_typevarcharN2007-04-16 19:35:06X 5-#3 UR::DataSource::Metamaindd_fk_constraintdata_sourcevarcharN2007-04-16 19:35:07T 5+3 UR::DataSource::Metamaindd_table_columnnullablevarcharN2007-04-16 19:35:06_ 5-13 UR::DataSource::Metamaindd_fk_constraintfk_constraint_namevarcharN2007-04-16 19:35:07T 5'3 UR::DataSource::Metamaindd_tablelast_ddl_timetimestampY2007-04-16 19:35:07^ 5;!3 UR::DataSource::Metamaindd_fk_constraint_columntable_namevarcharN2007-04-16 19:35:07] 5+/3 UR::DataSource::Metamaindd_bitmap_indexbitmap_index_namevarcharN2007-04-16 19:35:06b 5+53 UR::DataSource::Metamaindd_table_columnlast_object_revisiontimestampN2007-04-16 19:35:06^ 5;!3 UR::DataSource::Metamaindd_pk_constraint_columntable_namevarcharN2007-04-16 19:35:07 )cJ @w)795-!UR::DataSource::Metamaindd_fk_constrainttable_name-?5+/UR::DataSource::Metamaindd_bitmap_indexbitmap_index_name 95+#UR::DataSource::Metamaindd_bitmap_indexdata_source35+UR::DataSource::Metamaindd_bitmap_indexowner85+!UR::DataSource::Metamaindd_bitmap_indextable_name:5-#UR::DataSource::Metamaindd_fk_constraintdata_sourceA5-1UR::DataSource::Metamaindd_fk_constraintfk_constraint_nameC5-5UR::DataSource::Metamaindd_fk_constraintlast_object_revision,45-UR::DataSource::Metamaindd_fk_constraintowner)65-UR::DataSource::Metamaindd_fk_constraintr_owner";5-%UR::DataSource::Metamaindd_fk_constraintr_table_nameA5;#UR::DataSource::Metamaindd_fk_constraint_columncolumn_name(A5;#UR::DataSource::Metamaindd_fk_constraint_columndata_sourceH5;1UR::DataSource::Metamaindd_fk_constraint_columnfk_constraint_name';5;UR::DataSource::Metamaindd_fk_constraint_columnownerC5;'UR::DataSource::Metamaindd_fk_constraint_columnr_column_name% 9{3f3bv45'UR::DataSource::Metamaindd_tablelast_ddl_time;55UR::DataSource::Metamaindd_tablelast_object_revision,5UR::DataSource@5;!UR::DataSource::Metamaindd_fk_constraint_columntable_nameA5;#UR::DataSource::Metamaindd_pk_constraint_columncolumn_name&A5;#UR::DataSource::Metamaindd_pk_constraint_columndata_source;5;UR::DataSource::Metamaindd_pk_constraint_columnowner:5;UR::DataSource::Metamaindd_pk_constraint_columnrank@5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name 25#UR::DataSource::Metamaindd_tabledata_source$.5UR::DataSource::Metamaindd_tableer_type45'UR::DataSource::Metamaindd_tablelast_ddl_time;55UR::DataSource::Metamaindd_tablelast_object_revision,5UR::DataSource::Metamaindd_tableowner.5UR::DataSource::Metamaindd_tableremarks15!UR::DataSource::Metamaindd_tabletable_name15!UR::DataSource::Metamaindd_tabletable_type95+#UR::DataSource::Metamaindd_table_columncolumn_name DaPED` 5;%3 UR::DataSource::Metamaindd_fk_constraint_columnr_table_namevarcharN2007-04-16 19:35:07L 53 UR::DataSource::Metamaindd_tableremarksvarcharY2007-04-16 19:35:07O 5!3 UR::DataSource::Metamaindd_tabletable_typevarcharN2007-04-16 19:35:07O 5!3 UR::DataSource::Metamaindd_tabletable_namevarcharN2007-04-16 19:35:07W 5+#3 UR::DataSource::Metamaindd_table_columncolumn_namevarcharN2007-04-16 19:35:06_ 5;#3 UR::DataSource::Metamaindd_pk_constraint_columndata_sourcevarcharN2007-04-16 19:35:07S 5+3 UR::DataSource::Metamaindd_table_columnremarksvarcharY2007-04-16 19:35:06Y 5-%3 UR::DataSource::Metamaindd_fk_constraintr_table_namevarcharN2007-04-16 19:35:07_ 5;#3 UR::DataSource::Metamaindd_fk_constraint_columndata_sourcevarcharN2007-04-16 19:35:07J 53 UR::DataSource::Metamaindd_tableownervarcharY2007-04-16 19:35:07Q 5+3 UR::DataSource::Metamaindd_bitmap_indexownervarcharY2007-04-16 19:35:06 [H<x[R) 5-3 UR::DataSource::Metamaindd_fk_constraintownervarcharY2007-04-16 19:35:07_( 5;#3 UR::DataSource::Metamaindd_fk_constraint_columncolumn_namevarcharN2007-04-16 19:35:07f' 5;13 UR::DataSource::Metamaindd_fk_constraint_columnfk_constraint_namevarcharN2007-04-16 19:35:07_& 5;#3 UR::DataSource::Metamaindd_pk_constraint_columncolumn_namevarcharN2007-04-16 19:35:07a% 5;'3 UR::DataSource::Metamaindd_fk_constraint_columnr_column_namevarcharN2007-04-16 19:35:07P$ 5#3 UR::DataSource::Metamaindd_tabledata_sourcevarcharN2007-04-16 19:35:07b# 5C!3 UR::DataSource::Metamaindd_unique_constraint_columntable_namevarcharN2007-04-16 19:35:06T" 5-3 UR::DataSource::Metamaindd_fk_constraintr_ownervarcharY2007-04-16 19:35:07W! 5+#3 UR::DataSource::Metamaindd_table_columndata_sourcevarcharN2007-04-16 19:35:06] 5C3 UR::DataSource::Metamaindd_unique_constraint_columnownervarcharY2007-04-16 19:35:06 P6l5{95+#UR::DataSource::Metamaindd_table_columndata_source!75+UR::DataSource::Metamaindd_table_columndata_typeB5+5UR::DataSource::Metamaindd_table_columnlast_object_revision 65+UR::DataSource::Metamaindd_table_columnnullable35+UR::DataSource::Metamaindd_table_columnowner+55+UR::DataSource::Metamaindd_table_columnremarks85+!UR::DataSource::Metamaindd_table_columntable_nameE5C#UR::DataSource::Metamaindd_unique_constraint_columncolumn_name I5C+UR::DataSource::Metamaindd_unique_constraint_columnconstraint_nameE5C#UR::DataSource::Metamaindd_unique_constraint_columndata_source*?5CUR::DataSource::Metamaindd_unique_constraint_columnowner D5C!UR::DataSource::Metamaindd_unique_constraint_columntable_name# HW- 5-!3 UR::DataSource::Metamaindd_fk_constrainttable_namevarcharN2007-04-16 19:35:07c, 5-53 UR::DataSource::Metamaindd_fk_constraintlast_object_revisiontimestampN2007-04-16 19:35:07Q+ 5+3 UR::DataSource::Metamaindd_table_columnownervarcharY2007-04-16 19:35:06c* 5C#3 UR::DataSource::Metamaindd_unique_constraint_columndata_sourcevarcharN2007-04-16 19:35:06JTd/35+UR::DataSource::Metamaindd_bitmap_indexowner45-UR::DataSource::Metamaindd_fk_constraintowner:5-#UR::DataSource::Metamaindd_fk_constraintdata_source9 5+#UR::DataSource::Metamaindd_table_columncolumn_nameA 5;#UR::DataSource::Metamaindd_pk_constraint_columndata_source; 5;UR::DataSource::Metamaindd_fk_constraint_columnowner: 5;UR::DataSource::Metamaindd_pk_constraint_columnrankA 5;#UR::DataSource::Metamaindd_fk_constraint_columncolumn_name85+!UR::DataSource::Metamaindd_bitmap_indextable_name65-UR::DataSource::Metamaindd_fk_constraintr_owner@5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name95-!UR::DataSource::Metamaindd_fk_constrainttable_name;5;UR::DataSource::Metamaindd_pk_constraint_columnowner85+!UR::DataSource::Metamaindd_table_columntable_nameA5;#UR::DataSource::Metamaindd_fk_constraint_columndata_sourceE5C#UR::DataSource::Metamaindd_unique_constraint_columncoqqtI|965-UR::DataSource::Metamaindd_fk_constraintowner<5-#UR::DataSource::Metamaindd_fk_constraintdata_source;5+#UR::DataSource::Metamaindd_table_columncolumn_name C5;#UR::DataSource::Metamaindd_pk_constraint_columndata_source =5;UR::DataSource::Metamaindd_fk_constraint_columnowner <5;UR::DataSource::Metamaindd_pk_constraint_columnrank C5;#UR::DataSource::Metamaindd_fk_constraint_columncolumn_name :5+!UR::DataSource::Metamaindd_bitmap_indextable_name85-UR::DataSource::Metamaindd_fk_constraintr_ownerB5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name;5-!UR::DataSource::Metamaindd_fk_constrainttable_name=5;UR::DataSource::Metamaindd_pk_constraint_columnowner:5+!UR::DataSource::Metamaindd_table_columntable_nam;5+#UR::DataSource::Metamaindd_table_columndata_sourceJ5;1UR::DataSource::Metamaindd_fk_constraint_columnfk_constraint_name ` J<x3~A5+/A5+/UR::DataSource::Metamaindd_bitmap_indexbitmap_index_name;5+#UR::DataSource::Metamaindd_bitmap_indexdata_source55+UR::DataSource::Metamaindd_bitmap_indexowner:5+!UR::DataSource::MA5+/UR::DataSource::Metamaindd_bitmap_indexbitmap_index_name;5+#UR::DataSource::Metamaindd_bitmap_indexdata_source55+UR::DataSource::Metamaindd_bitmap_indexowner:5+!UR::DataSource::Metamaindd_bitmap_indextable_name<5-#UR::DataSource::Metamaindd_fk_constraintdata_sourceC5-1UR::DataSource::Metamaindd_fk_constraintfk_constraint_name65-UR::DataSource::Metamaindd_fk_constraintowner85-UR::DataSource::Metamaindd_fk_constraintr_owner=5-%UR::DataSource::Metamaindd_fk_constraintr_table_name;5-!UR::DataSource::Metamaindd_fk_constrainttable_nameC5;#UR::DataSource::Metamaindd_fk_constraint_columncolumn_name C5;#UR::DataSource::Metamaindd_fk_constraint_columndata_source ee*n,a/w35!UR::DataSource::Metam=5;UR::DataSource::Metamaindd_pk_constraint_columnowner<5;UR::DataSource::Metamaindd_pk_constraint_columnrank B5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name45#UR::DataSource::Metamaindd_tabledata_source.5UR::DataSource=5;UR::DataSource::Metamaindd_fk_constraint_columnowner B5;!UR::DataSource::Metamaindd_fk_constraint_columntable_name C5;#UR::DataSource::Metamaindd_pk_constraint_columncolumn_nameC5;#UR::DataSource::Metamaindd_pk_constraint_columndata_source =5;UR::DataSource::Metamaindd_pk_constraint_columnowner<5;UR::DataSource::Metamaindd_pk_constraint_columnrank B5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name45#UR::DataSource::Metamaindd_tabledata_source.5UR::DataSource::Metamaindd_tableowner35!UR::DataSource::Metamaindd_tabletable_name;5+#UR::DataSource::Metamaindd_table_columncolumn_name /v<JTd/35+UR::DataSource::Metamaindd_bitmap_indexowner45-UR::DataSource::Metamaindd_fk_constraintowner:5-#UR::DataSource::Metamaindd_fk_constraintdata_source9 5+#UR::DataSource::Metamaindd_table_columncolumn_nameA 5;#UR::DataSource::Metamaindd_pk_constraint_columndata_source; 5;UR::DataSource::Metamaindd_fk_constraint_columnowner: 5;UR::DataSource::Metamaindd_pk_constraint_columnrankA 5;#UR::DataSource::Metamaindd_fk_constraint_columncolumn_name85+!UR::DataSource::Metamaindd_bitmap_indextable_name65-UR::DataSource::Metamaindd_fk_constraintr_owner@5;!UR::DataSource::Metamaindd_pk_constraint_columntable_name95-!UR::DataSource::Metamaindd_fk_constrainttable_name;5;UR::DataSource::Metamaindd_pk_constraint_columnowner85+!UR::DataSource::Metamaindd_table_columntable_nameA5;#UR::DataSource::Metamaindd_fk_constraint_columndata_sourceE5C#UR::DataSource::Metamaindd_unique_constraint_columncolumn_name Yn+Zs2Y;5-%UR::DataSource::Metamaindd_fk_constraintr_table_name,5UR::DataSource::Metamaindd_tableowner95+#UR::DataSource::Metamaindd_table_columndata_source15!UR::DataSource::Metamaindd_tabletable_name?5+/UR::DataSource::Metamaindd_bitmap_indexbitmap_index_name25#UR::DataSource::Metamaindd_tabledata_source95+#UR::DataSource::Metamaindd_bitmap_indexdata_source35+UR::DataSource::Metamaindd_table_columnownerA5;#UR::DataSource::Metamaindd_pk_constraint_columncolumn_name?5CUR::DataSource::Metamaindd_unique_constraint_columnownerD5C!UR::DataSource::Metamaindd_unique_constraint_columntable_nameH5;1UR::DataSource::Metamaindd_fk_constraint_columnfk_constraint_nameA5-1UR::DataSource::Metamaindd_fk_constraintfk_constraint_nameI5C+UR::DataSource::Metamaindd_unique_constraint_columnconstraint_nameE5C#UR::DataSource::Metamaindd_unique_constraint_columndata_source **`/w55+UR::DataSource::Metamaindd_table_columnowner:5+!UR::DataSource::Metamaindd_table_columntable_nameG5C#UR::DataSource::Metamaindd_unique_constraint_columncolumn_nameK5C+UR::DataSource::Metamaindd_unique_constraint_columnconstraint_nameG5C#UR::DataSource::Metamaindd_unique_constraint_columndata_sourceA5CUR::DataSource::Metamaindd_unique_constraint_columnownerF5C!UR::DataSource::Metamaindd_unique_constraint_columntable_name @ 5;!UR::DataSource::Metamaindd_fk_constraint_columntable_name   plX CC7tabledd_unique_constraint_columndd_unique_constraint_columnCREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, X CC7tabledd_unique_constraint_columndd_unique_constraint_columnCREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) )U iCindexsqlite_autoindex_dd_unique_constraint_column_1dd_unique_constraint_column --Mtabledd_fk_constraintdd_fk_constraintCREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) )      ? S-indexsqlite_autoindex_dd_fk_constraint_1dd_fk_constraint# ;;]tabledd_fk_constraint_columndd_fk_constraint_columnCREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) )Ma;indexsqlite_autoindex_dd_fk_constraint_column_1dd_fk_constraint_column Filesystem.pm000444023532023421 23766512121654175 17515 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::Filesystem; use UR; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; use File::Basename; use File::Path; use List::Util; use Scalar::Util; use Errno qw(EINTR EAGAIN EOPNOTSUPP); # lets you specify the server in several ways: # path => '/path/name' # means there is one file storing the data # path => [ '/path1/name', '/path2/name' ] # means the first tile we need to open the file, pick one (for load balancing) # path => '/path/to/directory/' # means that directory contains one or more files, and the classes using # this datasource can have table_name metadata to pick the file # path => '/path/$param1/${param2}.ext' # means the values for $param1 and $param2 should come from the input rule. # If the rule doesn't specify the param, then it should glob for the possible # names at that point in the filesystem # path => '/path/&method/filename' # means the value for that part of the path should come from a method call # run as $subject_class_name->$method($rule) # path => '/path/*/path/$name/ # means it should glob at the appropriate time for the '*', but no use the # paths found matching the glob to infer any values # maybe suppert a URI scheme like # file:/path/$to/File.ext?columns=[a,b,c]&sorted_columns=[a,b] # TODO # * Support non-equality operators for properties that are part of the path spec class UR::DataSource::Filesystem { is => 'UR::DataSource', has => [ path => { doc => 'Path spec for the path on the filesystem containing the data' }, delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, header_lines => { is => 'Integer', default_value => 0, doc => 'Number of lines at the start of the file to skip' }, columns_from_header => { is => 'Boolean', default_value => 0, doc => 'The column names are in the first line of the file' }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, ], has_optional => [ columns => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, sorted_columns => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, ], doc => 'A data source for treating files as relational data', }; sub can_savepoint { 0;} # Doesn't support savepoints sub _regex { my $self = shift; unless ($self->{'_regex'}) { my $delimiter = $self->delimiter; my $r = eval { qr($delimiter) }; if ($@ || !$r) { $self->error_message("Unable to interepret delimiter '".$self->delimiter.": $@"); return; } $self->{'_regex'} = $r; } return $self->{'_regex'}; } sub _logger { my $self = shift; my $varname = shift; if ($ENV{$varname}) { my $log_fh = UR::DBI->sql_fh; return sub { my $msg = shift; my $time = time(); $msg =~ s/\b\$time\b/$time/g; my $localtime = scalar(localtime $time); $msg =~ s/\b\$localtime\b/$localtime/; $log_fh->print($msg); }; } else { return \&UR::Util::null_sub; } } # The behavior for handling the filehandles after fork is contained in # the read_record_from_file closure. There's nothing special for the # data source to do sub prepare_for_fork { return 1; } sub finish_up_after_fork { return 1; } # Like UR::BoolExpr::specifies_value_for, but works on either a BoolExpr # or another object. In the latter case, it returns true if the object's # class has the given property sub __specifies_value_for { my($self, $thing, $property_name) = @_; return $thing->isa('UR::BoolExpr') ? $thing->specifies_value_for($property_name) : $thing->__meta__->property_meta_for_name($property_name); } # Like UR::BoolExpr::value_for, but works on either a BoolExpr # or another object. sub __value_for { my($self, $thing, $property_name) = @_; return $thing->isa('UR::BoolExpr') ? $thing->value_for($property_name) : $thing->$property_name; } # Like UR::BoolExpr::subject_class_name, but works on either a BoolExpr # or another object. sub __subject_class_name { my($self, $thing) = @_; return $thing->isa('UR::BoolExpr') ? $thing->subject_class_name() : $thing->class; } sub _replace_vars_with_values_in_pathname { my($self, $rule_or_obj, $string, $prop_values_hash) = @_; $prop_values_hash ||= {}; # Match something like /some/path/$var/name or /some/path${var}.ext/name if ($string =~ m/\$\{?(\w+)\}?/) { my $varname = $1; my $subject_class_name = $self->__subject_class_name($rule_or_obj); unless ($subject_class_name->__meta__->property_meta_for_name($varname)) { Carp::croak("Invalid 'server' for data source ".$self->id . ": Path spec $string requires a value for property $varname " . " which is not a property of class $subject_class_name"); } my @string_replacement_values; if ($self->__specifies_value_for($rule_or_obj, $varname)) { my @property_values = $self->__value_for($rule_or_obj, $varname); if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # Make a listref that has one element per value for that property in the rule (in-clause # rules may have more than one value) # Each element has 2 parts, first is the value, second is the accumulated prop_values_hash # where we've added the occurance of this property havine one of the values @property_values = map { [ $_, { %$prop_values_hash, $varname => $_ } ] } @property_values; # Escape any shell glob characters in the values: [ ] { } ~ ? * and \ # we don't want a property with value '?' to be a glob wildcard @string_replacement_values = map { $_->[0] =~ s/([[\]{}~?*\\])/\\$1/; $_ } @property_values; } else { # The rule doesn't have a value for this property. # Put a shell wildcard in here, and a later glob will match things # The '.__glob_positions__' key holds a list of places we've inserted shell globs. # Each element is a 2-element list: index 0 is the string position, element 1 if the variable name. # This is needed so the later glob expansion can tell the difference between globs # that are part of the original path spec, and globs put in here my @glob_positions = @{ $prop_values_hash->{'.__glob_positions__'} || [] }; my $glob_pos = $-[0]; push @glob_positions, [$glob_pos, $varname]; @string_replacement_values = ([ '*', { %$prop_values_hash, '.__glob_positions__' => \@glob_positions} ]); } my @return = map { my $s = $string; substr($s, $-[0], $+[0] - $-[0], $_->[0]); [ $s, $_->[1] ]; } @string_replacement_values; # recursion to process the next variable replacement return map { $self->_replace_vars_with_values_in_pathname($rule_or_obj, @$_) } @return; } else { return [ $string, $prop_values_hash ]; } } sub _replace_subs_with_values_in_pathname { my($self, $rule_or_obj, $string, $prop_values_hash) = @_; $prop_values_hash ||= {}; my $subject_class_name = $self->__subject_class_name($rule_or_obj); # Match something like /some/path/&sub/name or /some/path&{sub}.ext/name if ($string =~ m/\&\{?(\w+)\}?/) { my $subname = $1; unless ($subject_class_name->can($subname)) { Carp::croak("Invalid 'server' for data source ".$self->id . ": Path spec $string requires a value for method $subname " . " which is not a method of class " . $self->__subject_class_name($rule_or_obj)); } my @property_values = eval { $subject_class_name->$subname($rule_or_obj) }; if ($@) { Carp::croak("Can't resolve final path for 'server' for data source ".$self->id . ": Method call to ${subject_class_name}::${subname} died with: $@"); } if (@property_values == 1 and ref($property_values[0]) eq 'ARRAY') { @property_values = @{$property_values[0]}; } # Make a listref that has one element per value for that property in the rule (in-clause # rules may have more than one value) # Each element has 2 parts, first is the value, second is the accumulated prop_values_hash # where we've added the occurance of this property havine one of the values @property_values = map { [ $_, { %$prop_values_hash } ] } @property_values; # Escape any shell glob characters in the values: [ ] { } ~ ? * and \ # we don't want a return value '?' or '*' to be a glob wildcard my @string_replacement_values = map { $_->[0] =~ s/([[\]{}~?*\\])/\\$1/; $_ } @property_values; # Given a pathname returned from the glob, return a new glob_position_list # that has fixed up the position information accounting for the fact that # the globbed pathname is a different length than the original spec my $original_path_length = length($string); my $glob_position_list = $prop_values_hash->{'.__glob_positions__'}; my $subname_replacement_position = $-[0]; my $fix_offsets_in_glob_list = sub { my $pathname = shift; # alter the position only if it is greater than the position of # the subname we're replacing return map { [ $_->[0] < $subname_replacement_position ? $_->[0] : $_->[0] + length($pathname) - $original_path_length, $_->[1] ] } @$glob_position_list; }; my @return = map { my $s = $string; substr($s, $-[0], $+[0] - $-[0], $_->[0]); $_->[1]->{'.__glob_positions__'} = [ $fix_offsets_in_glob_list->($s) ]; [ $s, $_->[1] ]; } @string_replacement_values; # recursion to process the next method call return map { $self->_replace_subs_with_values_in_pathname($rule_or_obj, @$_) } @return; } else { return [ $string, $prop_values_hash ]; } } sub _replace_glob_with_values_in_pathname { my($self, $string, $prop_values_hash) = @_; # a * not preceeded by a backslash, delimited by / if ($string =~ m#([^/]*?[^\\/]?(\*)[^/]*)#) { my $glob_pos = $-[2]; my $path_segment_including_glob = substr($string, 0, $+[0]); my $remaining_path = substr($string, $+[0]); my @glob_matches = map { $_ . $remaining_path } glob($path_segment_including_glob); my $resolve_glob_values_for_each_result; my $glob_position_list = $prop_values_hash->{'.__glob_positions__'}; # Given a pathname returned from the glob, return a new glob_position_list # that has fixed up the position information accounting for the fact that # the globbed pathname is a different length than the original spec my $original_path_length = length($string); my $fix_offsets_in_glob_list = sub { my $pathname = shift; return map { [ $_->[0] + length($pathname) - $original_path_length, $_->[1] ] } @$glob_position_list; }; if ($glob_position_list->[0]->[0] == $glob_pos) { # This * was put in previously by a $propname in the spec that wasn't mentioned in the rule my $path_delim_pos = index($path_segment_including_glob, '/', $glob_pos); $path_delim_pos = length($path_segment_including_glob) if ($path_delim_pos == -1); # No more /s my $regex_as_str = $path_segment_including_glob; # Find out just how many *s we're dealing with and where they are, up to the next / # remove them from the glob_position_list because we're going to resolve their values my(@glob_positions, @property_names); while (@$glob_position_list and $glob_position_list->[0]->[0] < $path_delim_pos ) { my $this_glob_info = shift @{$glob_position_list}; push @glob_positions, $this_glob_info->[0]; push @property_names, $this_glob_info->[1]; } # Replace the *s found with regex captures my $glob_replacement = '([^/]*)'; my $glob_rpl_offset = 0; my $offset_inc = length($glob_replacement) - 1; # replacing a 1-char string '*' with a 7-char string '([^/]*)' $regex_as_str = List::Util::reduce( sub { substr($a, $b + $glob_rpl_offset, 1, $glob_replacement); $glob_rpl_offset += $offset_inc; $a; }, ($regex_as_str, @glob_positions) ); my $regex = qr{$regex_as_str}; my @property_values_for_each_glob_match = map { [ $_, [ $_ =~ $regex] ] } @glob_matches; # Fill in the property names into .__glob_positions__ # we've resolved in this iteration, and apply offset fixups for the # difference in string length between the pre- and post-glob pathnames $resolve_glob_values_for_each_result = sub { return map { my %h = %$prop_values_hash; @h{@property_names} = @{$_->[1]}; $h{'.__glob_positions__'} = [ $fix_offsets_in_glob_list->($_->[0]) ]; [$_->[0], \%h]; } @property_values_for_each_glob_match; }; } else { # This is a glob put in the original path spec # The new path comes from the @glob_matches list. # Apply offset fixups for the difference in string length between the # pre- and post-glob pathnames $resolve_glob_values_for_each_result = sub { return map { [ $_, { %$prop_values_hash, '.__glob_positions__' => [ $fix_offsets_in_glob_list->($_) ] } ] } @glob_matches; }; } my @resolved_paths_and_property_values = $resolve_glob_values_for_each_result->(); # Recursion to process the next glob return map { $self->_replace_glob_with_values_in_pathname( @$_ ) } @resolved_paths_and_property_values; } else { delete $prop_values_hash->{'.__glob_positions__'}; return [ $string, $prop_values_hash ]; } } sub resolve_file_info_for_rule_and_path_spec { my($self, $rule, $path_spec) = @_; $path_spec ||= $self->path; return map { $self->_replace_glob_with_values_in_pathname(@$_) } map { $self->_replace_subs_with_values_in_pathname($rule, @$_) } $self->_replace_vars_with_values_in_pathname($rule, $path_spec); } # We're overriding path() so the first time it's called, it will # pick one from the list and then stay with that one for the life # of the program sub path { my $self = shift; unless ($self->{'__cached_path'}) { my $path = $self->__path(); if (ref($path) and ref($path) eq 'ARRAY') { my $count = @$path; my $idx = $$ % $count; $self->{'_cached_path'} = $path->[$idx]; } else { $self->{'_cached_path'} = $path; } } return $self->{'_cached_path'}; } # Names of creation params that we should force to be listrefs our %creation_param_is_list = map { $_ => 1 } qw( columns sorted_columns ); sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; #unless (exists $ds_data->{'columns'}) { # User didn't specify columns in the file. Assumme every property is a column, and in the same order # We'll have to ask the class object for the column list the first time there's a query #} my %ds_creation_params; foreach my $param ( qw( path delimiter record_separator columns header_lines columns_from_header handle_class sorted_columns ) ) { if (exists $ds_data->{$param}) { if ($creation_param_is_list{$param} and ref($ds_data->{$param}) ne 'ARRAY') { $ds_creation_params{$param} = \( $ds_data->{$param} ); } else { $ds_creation_params{$param} = $ds_data->{$param}; } } } my $ds_id = UR::Object::Type->autogenerate_new_object_id_uuid(); my $ds_type = delete $ds_data->{'is'} || __PACKAGE__; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id ); return $ds; } sub _things_in_list_are_numeric { my $self = shift; foreach ( @{$_[0]} ) { return 0 if (! Scalar::Util::looks_like_number($_)); } return 1; } # Construct a closure to perform an operator test against the given value # The closures return 0 is the test is successful, -1 if unsuccessful but # the file's value was less than $value, and 1 if unsuccessful and greater. # The iterator that churns through the file knows that if it's comparing an # ID/sorted column, and the comparator returns 1 then we've gone past the # point where we can expect to ever find another successful match and we # should stop looking my $ALWAYS_FALSE = sub { -1 }; sub _comparator_for_operator_and_property { my($self,$property,$operator,$value) = @_; no warnings 'uninitialized'; # we're handling ''/undef/null specially below where it matters if ($operator eq 'between') { if ($value->[0] eq '' or $value->[1] eq '') { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { if ($value->[0] > $value->[1]) { # Will never be true Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # numeric 'between' comparison return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} < $value->[0]) { return -1; } elsif (${$_[0]} > $value->[1]) { return 1; } else { return 0; } }; } else { if ($value->[0] gt $value->[1]) { Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # A string 'between' comparison return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} lt $value->[0]) { return -1; } elsif (${$_[0]} gt $value->[1]) { return 1; } else { return 0; } }; } } elsif ($operator eq 'in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { # Numeric 'in' comparison returns undef if we're within the range of the list # but don't actually match any of the items in the list @$value = sort { $a <=> $b } @$value; # sort the values first return sub { return -1 if (${$_[0]} eq ''); if (${$_[0]} < $value->[0]) { return -1; } elsif (${$_[0]} > $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if ${$_[0]} == $_; } return -1; } }; } else { # A string 'in' comparison @$value = sort { $a cmp $b } @$value; return sub { if (${$_[0]} lt $value->[0]) { return -1; } elsif (${$_[0]} gt $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if ${$_[0]} eq $_; } return -1; } }; } } elsif ($operator eq 'not in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { return sub { return -1 if (${$_[0]} eq ''); foreach ( @$value ) { return -1 if ${$_[0]} == $_; } return 0; } } else { return sub { foreach ( @$value ) { return -1 if ${$_[0]} eq $_; } return 0; } } } elsif ($operator eq 'like') { # 'like' is always a string comparison. In addition, we can't know if we're ahead # or behind in the file's ID columns, so the only two return values are 0 and 1 return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false # Convert SQL-type wildcards to Perl-type wildcards # Convert a % to a *, and _ to ., unless they're preceeded by \ to escape them. # Not that this isn't precisely correct, as \\% should really mean a literal \ # followed by a wildcard, but we can't be correct in all cases without including # a real parser. This will catch most cases. $value =~ s/(?is_numeric and $self->_things_in_list_are_numeric([$value])) { # Basic numeric comparisons if ($operator eq '=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number return ${$_[0]} <=> $value; }; } elsif ($operator eq '<') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} < $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} <= $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} > $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if (${$_[0]} eq ''); # null always != a number ${$_[0]} >= $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { ${$_[0]} ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { ${$_[0]} ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { return 0 if (${$_[0]} eq ''); # null always != a number ${$_[0]} != $value ? 0 : -1; } } } else { # Basic string comparisons if ($operator eq '=') { return sub { return -1 if (${$_[0]} eq '' xor $value eq ''); return ${$_[0]} cmp $value; }; } elsif ($operator eq '<') { return sub { ${$_[0]} lt $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if (${$_[0]} eq '' or $value eq ''); ${$_[0]} le $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { ${$_[0]} gt $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if (${$_[0]} eq '' or $value eq ''); ${$_[0]} ge $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { ${$_[0]} ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { ${$_[0]} ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { ${$_[0]} ne $value ? 0 : -1; } } } } sub _properties_from_path_spec { my($self) = @_; unless (exists $self->{'__properties_from_path_spec'}) { my $path = $self->path; $path = $path->[0] if ref($path); my @property_names; while($path =~ m/\G\$\{?(\w+)\}?/) { push @property_names, $1; } $self->{'__properties_from_path_spec'} = \@property_names; } return @{ $self->{'__properties_from_path_spec'} }; } sub _generate_loading_templates_arrayref { my($self, $old_sql_cols) = @_; # Each elt in @$column_data is a quad: # [ $class_meta, $property_meta, $table_name, $object_num ] # Keep only the properties with columns (mostly just to remove UR::Object::id my @sql_cols = grep { $_->[1]->column_name } @$old_sql_cols; my $template_data = $self->SUPER::_generate_loading_templates_arrayref(\@sql_cols); return $template_data; } sub _resolve_column_names_from_pathname { my($self,$pathname,$fh) = @_; unless (exists($self->{'__column_names_from_pathname'}->{$pathname})) { if (my $column_names_in_order = $self->columns) { $self->{'__column_names_from_pathname'}->{$pathname} = $column_names_in_order; } else { my $record_separator = $self->record_separator(); my $line = $fh->getline(); $line =~ s/$record_separator$//; # chomp, but for any value # FIXME - to support record-oriented files, we need some replacement for this... my $split_regex = $self->_regex(); my @headers = split($split_regex, $line); $self->{'__column_names_from_pathname'}->{$pathname} = \@headers; } } return $self->{'__column_names_from_pathname'}->{$pathname}; } sub file_is_sorted_as_requested { my($self, $query_plan) = @_; my $sorted_columns = $self->sorted_columns || []; my $order_by_columns = $query_plan->order_by_columns(); for (my $i = 0; $i < @$order_by_columns; $i++) { next if ($order_by_columns->[$i] eq '$.'); # input line number is always sorted next if ($order_by_columns->[$i] eq '__FILE__'); return 0 if $i > $#$sorted_columns; if ($sorted_columns->[$i] ne $order_by_columns->[$i]) { return 0; } } return 1; } # FIXME - this is a copy of parts of _generate_class_data_for_loading from UR::DS::RDBMS sub _generate_class_data_for_loading { my ($self, $class_meta) = @_; my $parent_class_data = $self->SUPER::_generate_class_data_for_loading($class_meta); my @class_hierarchy = ($class_meta->class_name,$class_meta->ancestry_class_names); my $order_by_columns; do { my @id_column_names; for my $inheritance_class_name (@class_hierarchy) { my $inheritance_class_object = UR::Object::Type->get($inheritance_class_name); unless ($inheritance_class_object->table_name) { next; } @id_column_names = #map { # my $t = $inheritance_class_object->table_name; # ($t) = ($t =~ /(\S+)\s*$/); # $t . '.' . $_ #} grep { defined } map { my $p = $inheritance_class_object->property_meta_for_name($_); die ("No property $_ found for " . $inheritance_class_object->class_name . "?") unless $p; $p->column_name; } map { $_->property_name } grep { $_->column_name } $inheritance_class_object->direct_id_property_metas; last if (@id_column_names); } $order_by_columns = \@id_column_names; }; my(@all_table_properties, @direct_table_properties, $first_table_name, $subclassify_by); for my $co ( $class_meta, @{ $parent_class_data->{parent_class_objects} } ) { my $table_name = $co->table_name; next unless $table_name; $first_table_name ||= $co->table_name; # $sub_classification_method_name ||= $co->sub_classification_method_name; # $sub_classification_meta_class_name ||= $co->sub_classification_meta_class_name; $subclassify_by ||= $co->subclassify_by; my $sort_sub = sub ($$) { return $_[0]->property_name cmp $_[1]->property_name }; push @all_table_properties, map { [$co, $_, $table_name, 0 ] } sort $sort_sub grep { defined $_->column_name && $_->column_name ne '' } UR::Object::Property->get( class_name => $co->class_name ); @direct_table_properties = @all_table_properties if $class_meta eq $co; } my $class_data = { %$parent_class_data, order_by_columns => $order_by_columns, direct_table_properties => \@direct_table_properties, all_table_properties => \@all_table_properties, }; return $class_data; } # Needed for the QueryPlan's processing of order-by params # Params are a list of the 4-tuples [class-meta, prop-meta, table-name, object-num] sub _select_clause_columns_for_table_property_data { my $self = shift; return [ map { $_->[1]->column_name } @_ ]; } # Used to populate the %value_extractor_for_column_name hash # It should return a sub that, when given a row of data from the source, # returns the proper data from that row. # # It's expected to return a sub that accepts ($self, $row, $fh, $filename) # and return a reference to the right data. In most cases, it'll just pluck # out the $column_idx'th element from $@row, but we're using it # to attach special meaning to the $. token sub _create_value_extractor_for_column_name { my($self, $rule, $column_name, $column_idx) = @_; if ($column_name eq '$.') { return sub { my($self, $row, $fh, $filename) = @_; my $line_no = $fh->input_line_number(); return \$line_no; }; } elsif ($column_name eq '__FILE__') { return sub { my($self,$row,$fh,$filename) = @_; return \$filename; }; } else { return sub { my($self, $row, $fh, $filename) = @_; return \$row->[$column_idx]; }; } } sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $class_name = $rule->subject_class_name; my $class_meta = $class_name->__meta__; my $rule_template = $rule->template; # We're defering to the class metadata here because we don't yet know the # pathnames of the files we'll be reading from. If the columns_from_header flag # is set, then there's no way of knowing what the columns are until then my @column_names = grep { defined } map { $class_meta->column_for_property($_) } $class_meta->all_property_names; # FIXME - leaning on the sorted_columns property here means: # 1) It's useless when used where the path spec is a directory and # classes have table_names, since each file is likely to have different # columns # 2) If we ultimately end up reading from more than one file, all the files # must be sorted in the same way. It's possible the user has sorted each # file differently, though in practice it would make for a lot of trouble my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $col = $_; substr($col, 0, 1, ''); $column_is_sorted_descending{$col} = 1; $col; } else { $_; } } @{ $self->sorted_columns || [] }; my %sorted_column_names = map { $_ => 1 } @sorted_column_names; my @unsorted_column_names = grep { ! exists $sorted_column_names{$_} } @column_names; my @rule_column_names_in_order; # The order we should perform rule matches on - value is the name of the column in the file my @comparison_for_column; # closures to call to perform the match - same order as @rule_column_names_in_order my %rule_column_name_to_comparison_index; my(%property_for_column, %operator_for_column, %value_for_column); # These are used for logging my $resolve_comparator_for_column_name = sub { my $column_name = shift; my $property_name = $class_meta->property_for_column($column_name); return unless $rule->specifies_value_for($property_name); my $operator = $rule->operator_for($property_name) || '='; my $rule_value = $rule->value_for($property_name); $property_for_column{$column_name} = $property_name; $operator_for_column{$column_name} = $operator; $value_for_column{$column_name} = $rule_value; my $comp_function = $self->_comparator_for_operator_and_property( $class_meta->property($property_name), $operator, $rule_value); push @rule_column_names_in_order, $column_name; push @comparison_for_column, $comp_function; $rule_column_name_to_comparison_index{$column_name} = $#comparison_for_column; return 1; }; my $sorted_columns_in_rule_count; # How many columns we can consider when trying "the shortcut" for sorted data my %column_is_used_in_sorted_capacity; foreach my $column_name ( @sorted_column_names ) { if (! $resolve_comparator_for_column_name->($column_name) and ! defined($sorted_columns_in_rule_count) ) { # The first time we don't match a sorted column, record the index $sorted_columns_in_rule_count = scalar(@rule_column_names_in_order); } else { $column_is_used_in_sorted_capacity{$column_name} = ' (sorted)'; } } $sorted_columns_in_rule_count ||= scalar(@rule_column_names_in_order); foreach my $column_name ( @unsorted_column_names ) { $resolve_comparator_for_column_name->($column_name); } # sort them by filename my @possible_file_info_list = sort { $a->[0] cmp $b->[0] } $self->resolve_file_info_for_rule_and_path_spec($rule); my $table_name = $class_meta->table_name; if (defined($table_name) and $table_name ne '__default__') { # Tack the final file name onto the end if the class has a table name @possible_file_info_list = map { [ $_->[0] . "/$table_name", $_->[1] ] } @possible_file_info_list; } my $handle_class = $self->handle_class; my $use_quick_read = $handle_class eq 'IO::Handle'; my $split_regex = $self->_regex(); my $logger = $self->_logger('UR_DBI_MONITOR_SQL'); my $record_separator = $self->record_separator; my $monitor_start_time = Time::HiRes::time(); { no warnings 'uninitialized'; $logger->("\nFILE: starting query covering " . scalar(@possible_file_info_list)." files:\n\t" . join("\n\t", map { $_->[0] } @possible_file_info_list ) . "\nFILTERS: " . (scalar(@rule_column_names_in_order) ? join("\n\t", map { $_ . $column_is_used_in_sorted_capacity{$_} . " $operator_for_column{$_} " . (ref($value_for_column{$_}) eq 'ARRAY' ? '[' . join(',',@{$value_for_column{$_}}) .']' : $value_for_column{$_} ) } @rule_column_names_in_order) : '*none*') . "\n\n" ); } my $query_plan = $self->_resolve_query_plan($rule_template); if (@{ $query_plan->{'loading_templates'} } > 1) { Carp::croak(__PACKAGE__ . " does not support joins. The rule was $rule"); } my $loading_template = $query_plan->{loading_templates}->[0]; my @property_names_in_loading_template_order = @{ $loading_template->{'property_names'} }; my @column_names_in_loading_template_order = map { $class_meta->column_for_property($_) } @property_names_in_loading_template_order; my %property_name_to_resultset_index_map; my %column_name_to_resultset_index_map; for (my $i = 0; $i < @property_names_in_loading_template_order; $i++) { my $property_name = $property_names_in_loading_template_order[$i]; $property_name_to_resultset_index_map{$property_name} = $i; $column_name_to_resultset_index_map{$class_meta->column_for_property($property_name)} = $i; } my @iterator_for_each_file; foreach ( @possible_file_info_list ) { my $pathname = $_->[0]; my $property_values_from_path_spec = $_->[1]; my @properties_from_path_spec = keys %$property_values_from_path_spec; my @values_from_path_spec = values %$property_values_from_path_spec; my $pid = $$; # For tracking whether there's been a fork() my $fh = $handle_class->new($pathname); unless ($fh) { $logger->("FILE: Skipping $pathname because it did not open: $!\n"); next; # missing or unopenable files is not fatal } my $column_names_in_order = $self->_resolve_column_names_from_pathname($pathname,$fh); # %value_for_column_name holds subs that return the value for that column. For values # determined from the path resolver, save that value here. Most other values get plucked out # of the line read from the file. The remaining values are special tokens like $. and __FILE__. # These subs are used both for testing whether values read from the data source pass the rule # and for constructing the resultset passed up to the Context my %value_for_column_name; my %column_name_to_index_map; my $ordered_column_names_count = scalar(@$column_names_in_order); for (my $i = 0; $i < $ordered_column_names_count; $i++) { my $column_name = $column_names_in_order->[$i]; next unless (defined $column_name); $column_name_to_index_map{$column_name} = $i; $value_for_column_name{$column_name} = $self->_create_value_extractor_for_column_name($rule, $column_name, $i); } foreach ( '$.', '__FILE__' ) { $value_for_column_name{$_} = $self->_create_value_extractor_for_column_name($rule, $_, undef); $column_name_to_index_map{$_} = undef; } while (my($prop, $value) = each %$property_values_from_path_spec) { my $column = $class_meta->column_for_property($prop); $value_for_column_name{$column} = sub { return \$value }; $column_name_to_index_map{$column} = undef; } # Convert the column_name keys here to indexes into the comparison list my %column_for_this_comparison_is_sorted_descending = map { $rule_column_name_to_comparison_index{$_} => $column_is_sorted_descending{$_} } grep { exists $rule_column_name_to_comparison_index{$_} } keys %column_is_sorted_descending; # rule properties that aren't actually columns in the file should be # satisfied by the path resolution already, so we can strip them out of the # list of columns to test my @rule_columns_in_order = map { $column_name_to_index_map{$_} } grep { exists $column_name_to_index_map{$_} } @rule_column_names_in_order; # And also strip out any items in @comparison_for_column for non-column data my @comparison_for_column_this_file = map { $comparison_for_column[ $rule_column_name_to_comparison_index{$_} ] } grep { exists $column_name_to_index_map{$_} } @rule_column_names_in_order; # Burn through the requsite number of header lines my $lines_read = $fh->input_line_number; my $throwaway_line_count = $self->header_lines; while($throwaway_line_count > $lines_read) { $lines_read++; scalar($fh->getline()); } my $lines_matched = 0; my $log_first_fetch; $log_first_fetch = sub { $logger->(sprintf("FILE: $pathname FIRST FETCH TIME: %.4f s\n\n", Time::HiRes::time() - $monitor_start_time)); $log_first_fetch = \&UR::Util::null_sub; }; my $log_first_match; $log_first_match = sub { $logger->("FILE: $pathname First match after reading $lines_read lines\n\n"); $log_first_match = \&UR::Util::null_sub; }; my $next_record; # This sub reads the next record (line) from the file, splits the line into # columns and puts the data into @$next_record my $record_separator_re = qr($record_separator$); my $read_record_from_file = sub { # Make sure some wise guy hasn't changed this out from under us local $/ = $record_separator; if ($pid != $$) { # There's been a fork() between the original opening and now # This filehandle is no longer valid to read from, but tell() # should still report the right position my $pos = $fh->tell(); $logger->("FILE: reopening file $pathname and seeking to position $pos after fork()\n"); my $fh = $handle_class->new($pathname); unless ($fh) { $logger->("FILE: Reopening $pathname after fork() failed: $!\n"); return; # behave if we're at EOF } $fh->seek($pos, 0); # fast-forward to the old position $pid = $$; } my $line; READ_LINE_FROM_FILE: while(! defined($line)) { # Hack for OSX 10.5. # At EOF, the getline below will return undef. Most builds of Perl # will also set $! to 0 at EOF so you can distinguish between the cases # of EOF (which may have actually happened a while ago because of buffering) # and an actual read error. OSX 10.5's Perl does not, and so $! # retains whatever value it had after the last failed syscall, likely # a stat() while looking for a Perl module. This should have no effect # other platforms where you can't trust $! at arbitrary points in time # anyway $! = 0; $line = $use_quick_read ? <$fh> : $fh->getline(); if ($line and $line !~ $record_separator_re) { # Was a short read - probably at EOF # If the record_separator is a multi-char string, and the last # characters of $line are the first characters of the # record_separator, it's likely (though not certain) that the right # Thing to do is to remove the partial record separator. for (my $keep_chars = length($record_separator); $keep_chars > 0; $keep_chars--) { my $match_rs = substr($record_separator, 0, $keep_chars); if ($line =~ m/$match_rs$/) { substr($line, 0 - $keep_chars) = ''; last; } } } unless (defined $line) { if ($! && ! $fh->eof()) { redo READ_LINE_FROM_FILE if ($! == EAGAIN or $! == EINTR); Carp::croak("read failed for file $pathname: $!"); } # at EOF. Close up shop and remove this fh from the list #flock($fh,LOCK_UN); $fh = undef; $next_record = undef; $logger->("FILE: $pathname at EOF\n" . "FILE: $lines_read lines read for this request. $lines_matched matches in this file\n" . sprintf("FILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n\n", Time::HiRes::time() - $monitor_start_time) ); return; } } $lines_read++; $line =~ s/$record_separator$//; # chomp, but for any value # FIXME - to support record-oriented files, we need some replacement for this... $next_record = [ split($split_regex, $line, $ordered_column_names_count) ]; }; my $number_of_comparisons = @comparison_for_column_this_file; # The file filter iterator. # This sub looks at @$next_record and applies the comparator functions in order. # If it passes all of them, it constructs a resultset row and passes it up to the # multiplexer iterator my $file_filter_iterator = sub { $log_first_fetch->(); FOR_EACH_LINE: for(1) { $read_record_from_file->(); unless ($next_record) { # Done reading from this file return; } for (my $i = 0; $i < $number_of_comparisons; $i++) { my $comparison = $comparison_for_column_this_file[$i]->( $value_for_column_name{ $rule_column_names_in_order[$i] }->($self, $next_record, $fh, $pathname) ); if ( ( ($column_for_this_comparison_is_sorted_descending{$i} and $comparison < 0) or $comparison > 0) and $i < $sorted_columns_in_rule_count ) { # We've gone past the last thing that could possibly match $logger->("FILE: $pathname $lines_read lines read for this request. $lines_matched matches\n" . sprintf("FILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time)); #flock($fh,LOCK_UN); return; } elsif ($comparison) { # comparison didn't match, read another line from the file redo FOR_EACH_LINE; } # That comparison worked... stay in the for() loop for other comparisons } } # All the comparisons return '0', meaning they passed $log_first_match->(); $lines_matched++; my @resultset = map { ref($_) ? $$_ : $_ } map { ref($value_for_column_name{$_}) ? $value_for_column_name{$_}->($self, $next_record, $fh, $pathname) : $value_for_column_name{$_} # constant value from path spec } @column_names_in_loading_template_order; return \@resultset; }; # Higher layers in the loading logic require rows from the data source to be returned # in ID order. If the file contents is not sorted primarily by ID, then we need to do # the less efficient thing by first reading in all the matching rows in one go, sorting # them by ID, then iterating over the results unless ($self->file_is_sorted_as_requested($query_plan)) { my @resultset_indexes_to_sort = map { $column_name_to_resultset_index_map{$_} } @{ $query_plan->order_by_columns() }; $file_filter_iterator = $self->_create_iterator_for_custom_sorted_columns($file_filter_iterator, $query_plan, \%column_name_to_resultset_index_map); } push @iterator_for_each_file, $file_filter_iterator; } if (! @iterator_for_each_file) { return \&UR::Util::null_sub; # No matching files } elsif (@iterator_for_each_file == 1) { return $iterator_for_each_file[0]; # If there's only 1 file, no need to multiplex } my @next_record_for_each_file; # in the same order as @iterator_for_each_file my %column_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } map { index($_, '-') == 0 ? substr($_, 1) : $_ } @{ $query_plan->order_by_columns }; my @resultset_index_sort_sub = map { &_resolve_sorter_for( is_numeric => $column_is_numeric{$_}, is_descending => $column_is_sorted_descending{$_}, column_index => $property_name_to_resultset_index_map{$_}); } @sorted_column_names; my %resultset_idx_is_sorted_descending = map { $column_name_to_resultset_index_map{$_} => 1 } keys %column_is_sorted_descending; my $resultset_sorter = sub { my($idx_a,$idx_b) = shift; foreach my $sort_sub ( @resultset_index_sort_sub ) { my $cmp = $sort_sub->($next_record_for_each_file[$idx_a], $next_record_for_each_file[$idx_b]); return $cmp if $cmp; # done if they're not equal } return 0; }; # This is the iterator returned to the Context, and knows about all the individual # file filter iterators. It compares the next resultset from each of them and # returns the next resultset to the Context my $multiplex_iterator = sub { return unless @iterator_for_each_file; # if they're all run out my $lowest_slot; for(my $i = 0; $i < @iterator_for_each_file; $i++) { unless(defined $next_record_for_each_file[$i]) { $next_record_for_each_file[$i] = $iterator_for_each_file[$i]->(); unless (defined $next_record_for_each_file[$i]) { # That iterator is exhausted, splice it out splice(@iterator_for_each_file, $i, 1); splice(@next_record_for_each_file, $i, 1); return unless (@iterator_for_each_file); # This can happen here if none of the files have matching data redo; } } unless (defined $lowest_slot) { $lowest_slot = $i; next; } my $cmp = $resultset_sorter->($lowest_slot, $i); if ($cmp > 0) { $lowest_slot = $i; } } my $retval = $next_record_for_each_file[$lowest_slot]; $next_record_for_each_file[$lowest_slot] = undef; return $retval; }; return $multiplex_iterator; } # Constructors for subs to sort appropriately sub _resolve_sorter_for { my %params = @_; my $col_idx = $params{'column_index'}; my $is_descending = (exists($params{'is_descending'}) && $params{'is_descending'}) || (exists($params{'is_ascending'}) && $params{'is_ascending'}); my $is_numeric = (exists($params{'is_numeric'}) && $params{'is_numeric'}) || (exists($params{'is_string'}) && $params{'is_string'}); if ($is_descending) { if ($is_numeric) { return sub($$) { $_[1]->[$col_idx] <=> $_[0]->[$col_idx] }; } else { return sub($$) { $_[1]->[$col_idx] cmp $_[0]->[$col_idx] }; } } else { if ($is_numeric) { return sub($$) { $_[0]->[$col_idx] <=> $_[1]->[$col_idx] }; } else { return sub($$) { $_[0]->[$col_idx] cmp $_[1]->[$col_idx] }; } } } # Higher layers in the loading logic require rows from the data source to be returned # in ID order. If the file contents is not sorted primarily by ID, then we need to do # the less efficient thing by first reading in all the matching rows in one go, sorting # them by ID, then iterating over the results sub _create_iterator_for_custom_sorted_columns { my($self, $iterator_this_file, $query_plan, $column_name_to_resultset_index_map) = @_; my @matching; while (my $row = $iterator_this_file->()) { push @matching, $row; # save matches as [id, rowref] } unless (@matching) { return \&UR::Util::null_sub; # Easy, no matches } my $class_meta = $query_plan->class_name->__meta__; my %column_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } map { index($_, '-') == 0 ? substr($_,1) : $_ } @{ $query_plan->order_by_columns }; my @sorters; { no warnings 'numeric'; no warnings 'uninitialized'; @sorters = map { &_resolve_sorter_for(%$_) } map { my $col_name = $_; my $descending = 0; if (index($col_name, '-') == 0) { $descending = 1; substr($col_name, 0, 1, ''); # remove the - } my $col_idx = $column_name_to_resultset_index_map->{$col_name}; { column_index => $col_idx, is_descending => $descending, is_numeric => $column_is_numeric{$col_name} }; } @{ $query_plan->order_by_columns }; } my $sort_by_order_by_columns; if (@sorters == 1) { $sort_by_order_by_columns = $sorters[0]; } else { $sort_by_order_by_columns = sub($$) { foreach (@sorters) { if (my $rv = $_->(@_)) { return $rv; } } return 0; }; } @matching = sort $sort_by_order_by_columns @matching; return sub { return shift @matching; }; } sub initializer_should_create_column_name_for_class_properties { 1; } # The string used to join fields of a row together when writing # # Since the 'delimiter' property is interpreted as a regex in the reading # code, we'll try to be smart about making a real string from that. # # subclasses can override this to provide a different implementation sub column_join_string { my $self = shift; my $join_pattern = $self->delimiter; # make some common substitutions... if ($join_pattern eq '\s*,\s*') { # The default... return ', '; } $join_pattern =~ s/\\s*//g; # Turn 0-or-more whitespaces to nothing $join_pattern =~ s/\\t/\t/; # tab $join_pattern =~ s/\\s/ /; # whitespace return $join_pattern; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { Carp::croak("Cannot call _sync_database as a class method on a non-singleton class"); } } $DB::single=1; my $changed_objects = delete $params{'changed_objects'}; my $path_spec = $self->path; # First, bin up the changed objects by their class' table_name my %objects_for_path; foreach my $obj ( @$changed_objects ) { my @path = $self->resolve_file_info_for_rule_and_path_spec($obj, $path_spec); if (!@path) { $self->error_message("Couldn't resolve destination file for object " .$obj->class." ID ".$obj->id.": ".Data::Dumper::Dumper($obj)); return; } elsif (@path > 1) { $self->error_message("Got multiple filenames when resolving destination file for object " . $obj->class." ID ".$obj->id.": ".join(', ', @path)); } $objects_for_path{ $path[0]->[0] } ||= []; push @{ $objects_for_path{ $path[0]->[0] } }, $obj; } my %objects_for_pathname; foreach my $path ( keys %objects_for_path ) { foreach my $obj ( @{ $objects_for_path{$path} } ) { my $class_meta = $obj->__meta__; my $table_name = $class_meta->table_name; my $pathname = $path; if (defined($table_name) and $table_name ne '__default__') { $pathname .= '/' . $table_name; } $objects_for_pathname{$pathname} ||= []; push @{ $objects_for_pathname{$pathname} }, $obj; } } my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $s = $_; substr($s, 0, 1, ''); $column_is_sorted_descending{$s} = $s; } else { $_; } } @{ $self->sorted_columns() || [] }; my $handle_class = $self->handle_class; my $use_quick_read = $handle_class->isa('IO::Handle'); my $join_string = $self->column_join_string; my $record_separator = $self->record_separator; my $split_regex = $self->_regex(); local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $logger = $self->_logger('UR_DBI_MONITOR_SQL'); my $total_save_time = Time::HiRes::time(); $logger->("FILE: Saving changes to ".scalar(keys %objects_for_pathname) . " files:\n\t" . join("\n\t", keys(%objects_for_pathname)) . "\n\n"); foreach my $pathname ( keys %objects_for_pathname ) { my $use_quick_rename; my $containing_directory = File::Basename::dirname($pathname); unless (-d $containing_directory) { File::Path::mkpath($containing_directory); } if (-w $containing_directory) { $use_quick_rename = 1; } elsif (! -w $pathname) { Carp::croak("Cannot save to file $pathname: Neither the directory nor the file are writable"); } my $read_fh = $handle_class->new($pathname); # Objects going to the same file should all be of a common class my $class_meta = $objects_for_pathname{$pathname}->[0]->__meta__; my @property_names_that_are_sorted = map { $class_meta->property_for_column($_) } @sorted_column_names; # Returns true of the passed-in object has a change in one of the sorted columns my $object_has_changed_sorted_column = sub { my $obj = shift; foreach my $prop ( @property_names_that_are_sorted ) { if (UR::Context->_get_committed_property_value($obj, $prop) ne $obj->$prop) { return 1; } } return 0; }; my $column_names_in_file = $self->_resolve_column_names_from_pathname($pathname, $read_fh); my $column_names_count = @$column_names_in_file; my %column_name_to_index; for (my $i = 0; $i < @$column_names_in_file; $i++) { $column_name_to_index{$column_names_in_file->[$i]} = $i; } # This lets us take a hash slice of the object and get a row for the file my @property_names_in_column_order = map { $class_meta->property_for_column($_) } @$column_names_in_file; my %column_name_is_numeric = map { $_->column_name => $_->is_numeric } map { $class_meta->property_meta_for_name($_) } map { $class_meta->property_for_column($_) } @$column_names_in_file; my $insert = []; my $update = {}; my $delete = {}; foreach my $obj ( @{ $objects_for_pathname{$pathname} } ) { if ($obj->isa('UR::Object::Ghost')) { # This should be removed from the file my $original = $obj->{'db_committed'}; my $line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } elsif ($obj->{'db_committed'}) { # this is a changed object my $original = $obj->{'db_committed'}; if ($object_has_changed_sorted_column->($obj)) { # One of hte sorted columns has changed. Model this as a delete and insert push @$insert, [ @{$obj}{@property_names_in_column_order} ]; my $line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } else { # This object is changed since it was read in the file my $original_line = join($join_string, @{$original}{@property_names_in_column_order}) . $record_separator; my $changed_line = join($join_string, @{$obj}{@property_names_in_column_order}) . $record_separator; $update->{$original_line} = $changed_line; } } else { # This object is new and should be added to the file push @$insert, [ @{$obj}{@property_names_in_column_order} ]; } } my %column_is_sorted_descending; my @sorted_column_names = map { if (index($_, '-') == 0) { my $s = $_; substr($s, 0, 1, ''); $column_is_sorted_descending{$s} = $s; } else { $_; } } @{ $self->sorted_columns() || [] }; my $row_sort_sub; if (@sorted_column_names) { my @comparison_subs = map { &_resolve_sorter_for(is_numeric => $column_name_is_numeric{$_}, is_descending => $column_is_sorted_descending{$_}, column_index => $column_name_to_index{$_}) } @sorted_column_names; $row_sort_sub = sub ($$) { foreach my $comparator ( @comparison_subs ) { my $cmp = $comparator->($_[0], $_[1]); return $cmp if $cmp; } return 0; }; # Put the rows-to-insert in sorted order my @insert_sorted = sort $row_sort_sub @$insert; $insert = \@insert_sorted; } my $write_fh = $use_quick_rename ? File::Temp->new(DIR => $containing_directory) : File::Temp->new(); unless ($write_fh) { Carp::croak("Can't save changes for $pathname: Can't create temporary file for writing: $!"); } my $monitor_start_rime = Time::HiRes::time(); my $time = time(); $logger->(sprintf("\nFILE: SYNC DATABASE AT %s [%s]. Started transaction for %s to temp file %s\n", $time, scalar(localtime($time)), $pathname, $write_fh->filename)); # Write headers to the new file for (my $i = 0; $i < $self->header_lines; $i++) { my $line = $use_quick_read ? <$read_fh> : $read_fh->getline(); $write_fh->print($line); } my $line; READ_A_LINE: while(1) { unless ($line) { $line = $use_quick_read ? <$read_fh> : $read_fh->getline(); last unless defined $line; } if (@sorted_column_names and scalar(@$insert)) { # There are sorted things waiting to insert my $chomped = $line; $chomped =~ s/$record_separator$//; # chomp, but for any value my $row = [ split($split_regex, $chomped, $column_names_count) ]; my $cmp = $row_sort_sub->($row, $insert->[0]); if ($cmp > 0) { # write the object's data no warnings 'uninitialized'; # Some of the object's data may be undef my $new_row = shift @$insert; my $new_line = join($join_string, @$new_row) . $record_separator; $logger->("FILE: INSERT >>$new_line<<\n"); $write_fh->print($new_line); # Don't undef the last line read, meaning it could still be written to the output... next READ_A_LINE; } } if (my $obj = delete $delete->{$line}) { $logger->("FILE: DELETE >>$line<<\n"); } elsif (my $changed = delete $update->{$line}) { $logger->("FILE: UPDFATE replace >>$line<< with >>$changed<<\n"); $write_fh->print($changed); } else { # This line form the file was unchanged in the app $write_fh->print($line); } $line = undef; } if (keys %$delete) { $self->warning_message("There were " . scalar( keys %$delete) . " deleted " . $class_meta->class_name . " objects that did not match data in file $pathname"); } if (keys %$update) { $self->warning_message("There were " . scalar( keys %$delete) . " updated " . $class_meta->class_name . " objects that did not match data in file $pathname"); } # finish out by writing the rest of the new data foreach my $new_row ( @$insert ) { no warnings 'uninitialized'; # Some of the object's data may be undef my $new_line = join($join_string, @$new_row) . $record_separator; $logger->("FILE: INSERT >>$new_line<<\n"); $write_fh->print($new_line); } my $changed_objects = $objects_for_pathname{$pathname}; unless ($self->_set_specified_objects_saved_uncommitted( $changed_objects )) { Carp::croak("Error setting objects to a saved state after syncing"); } # These closures will keep $write_fh in scope and delay their removal until # commit() or rollback(). Call these with no args to commit, and one arg (doesn't # matter what) to roll back my $commit = $use_quick_rename ? sub { if (@_) { $self->_set_specified_objects_saved_rolled_back($changed_objects); } else { my $temp_filename = $write_fh->filename; $logger->("FILE: COMMIT rename $temp_filename => $pathname\n"); unless (rename($temp_filename, $pathname)) { $self->error_message("Can't rename $temp_filename to $pathname: $!"); return; } $self->_set_specified_objects_saved_committed($changed_objects); } return 1; } : sub { if (@_) { $self->_set_specified_objects_saved_rolled_back($changed_objects); } else { my $temp_filename = $write_fh->filename; $logger->("FILE: COMMIT copy " . $temp_filename . " => $pathname\n"); my $read_fh = IO::File->new($temp_filename); unless ($read_fh) { $self->error_message("Can't open file $temp_filename for reading: $!"); return; } my $copy_fh = IO::File->new($pathname, 'w'); unless ($copy_fh) { $self->error_message("Can't open file $pathname for writing: $!"); return; } while(<$read_fh>) { $copy_fh->print($_); } $copy_fh->close(); $read_fh->close(); $self->_set_specified_objects_saved_committed($changed_objects); } return 1; }; $write_fh->close(); $self->{'__saved_uncommitted'} ||= []; push @{ $self->{'__saved_uncommitted'} }, $commit; $time = time(); $logger->("\nFILE: SYNC DATABASE finished ".$write_fh->filename . "\n"); } $logger->(sprintf("Saved changes to %d files in %.4f s\n", scalar(@{ $self->{'__saved_uncommitted'}}), Time::HiRes::time() - $total_save_time)); return 1; } sub commit { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } if ($self->{'__saved_uncommitted'}) { foreach my $commit ( @{ $self->{'__saved_uncommitted'}}) { $commit->(); } } delete $self->{'__saved_uncommitted'}; return 1; } sub rollback { my $self = shift; if (! ref($self) and $self->isa('UR::Singleton')) { $self = $self->_singleton_object; } if ($self->{'__saved_uncommitted'}) { foreach my $commit ( @{ $self->{'__saved_uncommitted'}}) { $commit->('rollback'); } } delete $self->{'__saved_uncommitted'}; return 1; } 1; __END__ =pod =head1 NAME UR::DataSource::Filesystem - Get and save objects to delimited text files =head1 SYNOPSIS # Create an object for the data file my $people_data = UR::DataSource::Filesystem->create( columns => ['person_id','name','age','street_address'], sorted_columns => ['age','person_id'], path => '/var/lib/people/$state/$city/people.txt', delimiter => "\t", # between columns in the file record_separator => "\n", # between lines in the file ); # Define an entity class for the people in the file class MyProgram::Person { id_by => 'person_id', has => [ name => { is => 'String' }, age => { is => 'Number' }, street_address => { is => 'String' }, city => { is => 'String' }, state => { is => 'String' }, ], data_source_id => $people_data->id, }; # Get all people that live in any city named Springfield older than 40 my @springfielders = MyProgram::Person->get(city => 'Springfield', 'age >' => 40); =head1 DESCRIPTION A Filesystem data source object represents one or more files on the fileystem. In the simplest case, the object's 'path' property names a file that stores the data. =head2 Properties These properties determine the configuration for the data source. =over 4 =item path path is a string representing the path to the files. Besides just being a simple pathname to one file, the string can also be a specification of many similar files, or a directory containing multiple files. See below for more information about 'path' =item record_separator The separator between lines in the file. This gets stored in $/ before calling getline() to read data. The default record_separator is "\n". =item delimiter The separator between columns in the file. It is used to construct a regex with qr() to split() a line into a list of values. The default delimiter is '\s*,\s*', meaning that the file is separated by commas. Another common value would be "\t" for tabs. =item columns A listref of column names in the file. Just as SQL tables have columns, Filesystem files also have named columns so the system knows how to read the file data into object properties. A Filesystem data source does not need to specify named columns if the 'columns_from_header' property is true. Classes that use the Filesystem data source attach their properties to the data source's columns via the 'column_name' metadata. Besides the columns directly named in the 'columns' list, two additional column-like tokens may be used as a column_name: '__FILE__' and '$.'. __FILE__ means the object's property will hold the name of the file the data was read from. $. means the value will be the input line number from the file. These are useful when iterating over the contents of a file. Since these two fake columns are always considered "sorted", it makes reading from the file faster in some cases. See the 'sorted_columns' discussion below for more information. =item sorted_columns A listref of column names that the file is sorted by, in the order of the sorting. If a column is sorted in descending order, put a minus (-) in front of the name. If the file is sorted by multiple columns, say first by last_name and then by first_name, then include them both: sorted_columns => ['last_name','first_name'] The system uses this information to know when to stop reading if a query is done on a sorted column. It's also used to determine whether a query done on the data source matches the sort order of the file. If not, then the data must be gathered in two passes. The first pass finds records in the file that match the filter. After that, the matching records are sorted in the same way the query is requesting before returning the data to the Context. The Context expects incoming data to always be sorted by at least the class' ID properties. If the file is unsorted and the caller wants to be able to iterate over the data, then it is common to have the class' ID properties specified like this: id_by => [ file => { is => 'String', column_name => '__FILE__' }, line => { is => 'Integer', column_name => '$.' }, ] Otherwise, it will need to read in the whole file and sort the contents before returning the first row of data from its iterator. =item columns_from_header If true, the system will read the first line of the file to determine what the column names are. =item header_lines The number of lines at the top of the file that do not contain entity data. When the file is opened, this number of lines are skipped before reading data. If the columns_from_header flag is true, the header_lines value should be at least 1. =item handle_class Which class to use for reading and writing to the file. The default is IO::File. Any other value must refer to a class that has the same interface as IO::File, in particular: new, input_line_number, getline, tell, seek and print. =back =head2 Path specification Besides refering to just one file on the filesystem, the path spec is a recipe for finding files in a directory tree. If a class using a Filesystem data source does not have 'table_name' metadata, then the path specification must resolve to file names. Alternatively, classes may specify their 'table_name' which is interpreted as a file within the directory indicated by the path specification. Three kinds of special tokens can also appear in a file spec: =over 4 =item $property When querying, the system will extract the value (or values, for an in-clause) of $property from the BoolExpr when constructing the pathname. If the BoolExpr does not have a value for that property, then the system will do a shell glob to find the possible values. For example, given this path spec and query: path => '/var/people/$state/$city/people.txt' my @people = MyProgram::People->get(city => 'Springfield', 'age >' => 40); it would find the data files using the glob expression /var/people/*/Springfield/people.txt It also knows that any objects coming from the file /var/people/CA/Springfield/people.txt must have the value 'CA' for their 'state' property, even though that information is not in the contents of the file. When committing changes back to the file, the object property values are used to determine which file it should be saved to. The property name can also be wrapped in braces: /var/people/${state}_US/city_${city}/people.txt =item &method The replacement value is resolved by calling the named method on the subject class of the query. The method is called like this: $replacement = $subject_class->$method( $boolexpr_or_object); During a query, the method is passed a BoolExpr; during a commit, the method is passed an object. It must return a string. The method name can also be wrapped in braces: /&{resolve_prefix}.dir/people.txt =item *, ? Literal shell glob wildcards are honored when finding files, but their values are not used to supply values to objects. =back =head2 Environment Variables If the environment variable $UR_DBI_MONITOR_SQL is true, then the Filesystem data source will print information about the queries it runs. =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource =cut MySQL.pm000444023532023421 1450712121654175 16302 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::MySQL; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::MySQL', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "mysql" } #sub server { # my $self = shift->_singleton_object(); # $self->_init_database; # return $self->_database_file_path; #} sub owner { shift->_singleton_object->login } #sub login { # undef #} # #sub auth { # undef #} sub _default_sql_like_escape_string { undef }; # can't do an 'escape' clause with the 'like' operator sub can_savepoint { 1;} sub _init_created_dbh { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(pg_|sql_|URMETA)/; } # # for concurrency's sake we need to use dummy tables in place of sequence generators here, too # sub _get_sequence_name_for_table_and_column { my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_SEQ', $table_name, $column_name); #$DB::single = 1; unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTO_INCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } sub get_bitmap_index_details_from_data_dictionary { # Mysql dosen't have bitmap indexes. return []; } sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("savepoint $sp_name"); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; my $sp = $dbh->quote($sp_name); $dbh->do("rollback to savepoint $sp_name"); } sub resolve_order_by_clause { my($self,$order_by_columns,$order_by_column_data) = @_; my @cols = @$order_by_columns; foreach my $col ( @cols) { my $is_descending; if ($col =~ m/^(-|\+)(.*)$/) { $col = $2; if ($1 eq '-') { $is_descending = 1; } } my $property_meta = $order_by_column_data->{$col} ? $order_by_column_data->{$col}->[1] : undef; my $is_optional; $is_optional = $property_meta->is_optional if $property_meta; if ($is_optional) { if ($is_descending) { $col = "CASE WHEN $col ISNULL THEN 0 ELSE 1 END, $col DESC"; } else { $col = "CASE WHEN $col ISNULL THEN 1 ELSE 0 END, $col"; } } elsif ($is_descending) { $col = $col . ' DESC'; } } return 'order by ' . join(', ',@cols); } # FIXME This works on Mysql 4.x (and later?). Mysql5 has a database called # IMFORMATION_SCHEMA that may be more useful for these kinds of queries sub get_unique_index_details_from_data_dictionary { my($self,$table_name) = @_; my $dbh = $self->get_default_handle(); return undef unless $dbh; #$table_name = $dbh->quote($table_name); my $sql = qq(SHOW INDEX FROM $table_name); my $sth = $dbh->prepare($sql); return undef unless $sth; $sth->execute(); my $ret; while (my $data = $sth->fetchrow_hashref()) { next if ($data->{'Non_unique'}); $ret->{$data->{'Key_name'}} ||= []; push @{ $ret->{ $data->{'Key_name'} } }, $data->{'Column_name'}; } return $ret; } sub get_column_details_from_data_dictionary { my $self = shift; # Mysql seems wierd about the distinction between catalog/database and schema/owner # For 'ur update classes', it works if we just pass in undef for catalog # The passed-in args are: $self,$catalog,$schema,$table,$column my $catalog = shift; return $self->SUPER::get_column_details_from_data_dictionary(undef, @_); } sub get_foreign_key_details_from_data_dictionary { my $self = shift; # Mysql requires undef in some fields instead of an empty string my @new_params = map { length($_) ? $_ : undef } @_; return $self->SUPER::get_foreign_key_details_from_data_dictionary(@new_params); } my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'TINYINT' => ['Integer', undef], 'SMALLINT' => ['Integer', undef], 'MEDIUMINT' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'BINARY' => ['Text', undef], 'VARBINARY' => ['Text', undef], 'TINYTEXT' => ['Text', undef], 'MEDIUMTEXT' => ['Text', undef], 'LONGTEXT' => ['Text', undef], 'TINYBLOB' => ['Blob', undef], 'MEDIUMBLOB' => ['Blob', undef], 'LONGBLOB' => ['Blob', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; my $urtype = $ur_data_type_for_vendor_data_type{uc($type)}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } 1; =pod =head1 NAME UR::DataSource::MySQL - MySQL specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the MySQL-specific methods necessary for interacting with MySQL databases =head1 SEE ALSO L, L =cut File.pm000444023532023421 14543412121654175 16240 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::File; # NOTE! This module is deprecated. Use UR::DataSource::Filesystem instead. # A data source implementation for text files where the fields # are delimited by commas (or anything else really). Usually, # the lines in the file will be sorted by one or more columns, # but it isn't strictly necessary # # For now, it's structured around files where the record is delimited by # newlines, and the fields are delimited by qr(\s*,\s*). Those are # overridable in concrete data sources by specifying record_seperator() and # delimiter(). # FIXME - work out a way to support record-oriented data as well as line-oriented data use UR; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; use Fcntl qw(:DEFAULT :flock); use Errno qw(EINTR EAGAIN EOPNOTSUPP); use File::Temp; use File::Basename; use IO::File qw(); our @CARP_NOT = qw( UR::Context UR::DataSource::FileMux UR::Object::Type ); class UR::DataSource::File { is => ['UR::DataSource'], has => [ delimiter => { is => 'String', default_value => '\s*,\s*', doc => 'Delimiter between columns on the same line' }, record_separator => { is => 'String', default_value => "\n", doc => 'Delimiter between lines in the file' }, column_order => { is => 'ARRAY', doc => 'Names of the columns in the file, in order' }, skip_first_line => { is => 'Integer', default_value => 0, doc => 'Number of lines at the start of the file to skip' }, handle_class => { is => 'String', default_value => 'IO::File', doc => 'Class to use for new file handles' }, quick_disconnect => { is => 'Boolean', default_value => 1, doc => 'Do not hold the file handle open between requests' }, ], has_optional => [ server => { is => 'String', doc => 'pathname to the data file' }, file_list => { is => 'ARRAY', doc => 'list of pathnames of equivalent files' }, sort_order => { is => 'ARRAY', doc => 'Names of the columns by which the data file is sorted' }, constant_values => { is => 'ARRAY', doc => 'Property names which are not in the data file(s), but are part of the objects loaded from the data source' }, # REMOVE #file_cache_index => { is => 'Integer', doc => 'index into the file cache where the next read will be placed' }, _open_query_count => { is => 'Integer', doc => 'number of queries currently using this data source, used internally' }, ], doc => 'A data source for line-oriented files', }; sub can_savepoint { 0;} # Doesn't support savepoints sub get_default_handle { my $self = shift; unless ($self->{'_fh'}) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { my $time = time(); UR::DBI->sql_fh->printf("\nFILE OPEN AT %d [%s]\n",$time, scalar(localtime($time))); } my $filename = $self->server; unless (-e $filename) { # file doesn't exist $filename = '/dev/null'; } my $handle_class = $self->handle_class; my $fh = $handle_class->new($filename); unless($fh) { $self->error_message("Can't open ".$self->server." for reading: $!"); return; } if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->printf("FILE: opened %s fileno %d\n\n",$self->server, $fh->fileno); } $self->{'_fh'} = $fh; $self->is_connected(1); } return $self->{'_fh'}; } sub disconnect_default_handle { my $self = shift; if (my $fh = $self->{'_fh'}) { flock($fh,LOCK_UN); $fh->close(); $self->{'_fh'} = undef; $self->is_connected(0); } } sub prepare_for_fork { my $self = shift; # make sure this is clear before we fork $self->{'_fh_position'} = undef; if (defined $self->{'_fh'}) { $self->{'_fh_position'} = $self->{'_fh'}->tell(); UR::DBI->sql_fh->printf("FILE: preparing to fork; closing file %s and noting position at %s\n",$self->server, $self->{'_fh_position'}) if $ENV{'UR_DBI_MONITOR_SQL'}; } $self->disconnect_default_handle; } sub finish_up_after_fork { my $self = shift; if (defined $self->{'_fh_position'}) { UR::DBI->sql_fh->printf("FILE: resetting after fork; reopening file %s and fast-forwarding to %s\n",$self->server, $self->{'_fh_position'}) if $ENV{'UR_DBI_MONITOR_SQL'}; my $fh = $self->get_default_handle; $fh->seek($self->{'_fh_position'},0); } } sub _regex { my $self = shift; unless ($self->{'_regex'}) { my $delimiter = $self->delimiter; my $r = eval { qr($delimiter) }; if ($@ || !$r) { $self->error_message("Unable to interepret delimiter '".$self->delimiter.": $@"); return; } $self->{'_regex'} = $r; } return $self->{'_regex'}; } # We're overriding server() so everyone else can have a single way of getting # the file's pathname instead of having to know about both server and file_list sub server { my $self = shift; unless ($self->{'_cached_server'}) { if ($self->__server()) { $self->{'_cached_server'} = $self->__server(); } elsif ($self->file_list) { my $files = $self->file_list; my $count = scalar(@$files); my $idx = $$ % $count; $self->{'_cached_server'} = $files->[$idx]; } else { die "Data source ",$self->id," didn't specify either server or file_list"; } } return $self->{'_cached_server'}; } # Should be divisible by 3 our $MAX_CACHE_SIZE = 99; # The offset cache is an arrayref containing three pieces of data: # 0: If this cache slot is being used by a loading iterator # 1: concatenated data from the sorted columns for comparison with where you are in the file # 2: the seek position that line came from sub _offset_cache { my $self = shift; unless ($self->{'_offset_cache'}) { $self->{'_offset_cache'} = []; } return $self->{'_offset_cache'}; } our %iterator_data_source; our %iterator_cache_slot_refs; sub _allocate_offset_cache_slot { my $self = shift; my $cache = $self->_offset_cache(); my $next = scalar(@$cache); #print STDERR "_allocate_offset_cache_slot ".$self->server." current size is $next "; if ($next > $MAX_CACHE_SIZE) { #print STDERR "searching... \n"; my $last_offset_cache_slot = $self->{'_last_offset_cache_slot'}; if ($last_offset_cache_slot >= $MAX_CACHE_SIZE) { $next = 0; } else { $next = $last_offset_cache_slot + 3; } # Search for an unused slot while ($cache->[$next] and $next != $last_offset_cache_slot) { $next += 3; $next = 0 if ($next > $MAX_CACHE_SIZE); } if ($next > $MAX_CACHE_SIZE or $next eq $last_offset_cache_slot) { #print STDERR scalar(keys(%iterator_data_source))." items in iterator_data_source ".scalar(keys(%iterator_cache_slot))." in iterator_cache_slot\n"; Carp::carp("Unable to find an open file offset cache slot because there are too many outstanding loading iterators. Temporarily expanding the cache..."); # We'll let it go ahead and expand the list $next = $MAX_CACHE_SIZE; $MAX_CACHE_SIZE += 3; } } $cache->[$next] = 1; $cache->[$next+1] = undef; $cache->[$next+2] = undef; $self->{'_last_offset_cache_slot'} = $next; #print STDERR "using slot $next current size ".scalar(@$cache)."\n"; return $next; } sub _free_offset_cache_slot { my($self, $cache_slot) = @_; my $cache = $self->_offset_cache(); unless ($cache_slot < scalar(@$cache)) { $self->warning_message("Freeing offset cache slot past the end. Current size ".scalar(@$cache).", requested $cache_slot"); return; } unless (defined $cache->[$cache_slot]) { $self->warning_message("Freeing unused offset cache slot $cache_slot"); return; } if ($cache->[$cache_slot+1] and scalar(@{$cache->[$cache_slot+1]}) == 0) { # There's no data in here. Must have happened when the reader went all the # way to the end of the file and found nothing. Remove this entry completely # because it's not helpful splice(@$cache, $cache_slot,3); } else { # There is data in here, mark it as a free slot $cache->[$cache_slot] = 0; } return 1; } sub _invalidate_cache { my $self = shift; $self->{'_offset_cache'} = []; return 1; } sub _generate_loading_templates_arrayref { my($self,$old_sql_cols) = @_; my $columns_in_file = $self->column_order; my %column_to_position_map; for (my $i = 0; $i < @$columns_in_file; $i++) { $column_to_position_map{$columns_in_file->[$i]} = $i; } # strip out columns that don't exist in the file my $sql_cols; foreach my $column_data ( @$old_sql_cols ) { my $propertys_column_name = $column_data->[1]->column_name; next unless ($propertys_column_name and exists($column_to_position_map{$propertys_column_name})); push @$sql_cols, $column_data; } unless ($sql_cols) { $self->error_message("Couldn't determine column information for data source " . $self->id); return; } # reorder the requested columns to be in the same order as the file my @sql_cols_with_column_name = map{ [ $column_to_position_map{ $_->[1]->column_name }, $_ ] } @$sql_cols; my @sorted_sql_cols = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @sql_cols_with_column_name; $sql_cols = \@sorted_sql_cols; my $templates = $self->SUPER::_generate_loading_templates_arrayref($sql_cols); if (my $constant_values = $self->constant_values) { # Find the first unused index in the loading template my $next_template_slot = -1; foreach my $tmpl ( @$templates ) { foreach my $col ( @{$tmpl->{'column_positions'}} ) { if ($col >= $next_template_slot) { $next_template_slot = $col + 1; } } } if ($next_template_slot == -1) { die "Couldn't determine last column in loading template for data source" . $self->id; } foreach my $prop ( @$constant_values ) { push @{$templates->[0]->{'column_positions'}}, $next_template_slot++; push @{$templates->[0]->{'property_names'}}, $prop; } } return $templates; } sub _things_in_list_are_numeric { my $self = shift; foreach ( @{$_[0]} ) { return 0 if (! Scalar::Util::looks_like_number($_)); } return 1; } # Construct a closure to perform a test on the $index-th column of # @$$next_candidate_row. The closures return 0 is the test is successful, # -1 if unsuccessful but the file's value was less than $value, and 1 # if unsuccessful and greater. The iterator that churns throug the file # knows that if it's comparing an ID/sorted column, and the comparator # returns 1 then we've gone past the point where we can expect to ever # find another successful match and we should stop looking my $ALWAYS_FALSE = sub { -1 }; sub _comparator_for_operator_and_property { my($self,$property,$next_candidate_row, $index, $operator,$value) = @_; no warnings 'uninitialized'; # we're handling ''/undef/null specially below where it matters if ($operator eq 'between') { if ($value->[0] eq '' or $value->[1] eq '') { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { if ($value->[0] > $value->[1]) { # Will never be true Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # numeric 'between' comparison return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] < $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] > $value->[1]) { return 1; } else { return 0; } }; } else { if ($value->[0] gt $value->[1]) { Carp::carp "'between' comparison will never be true with values ".$value->[0]," and ".$value->[1]; return $ALWAYS_FALSE; } # A string 'between' comparison return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] lt $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] gt $value->[1]) { return 1; } else { return 0; } }; } } elsif ($operator eq 'in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { # Numeric 'in' comparison returns undef if we're within the range of the list # but don't actually match any of the items in the list @$value = sort { $a <=> $b } @$value; # sort the values first return sub { return -1 if ($$next_candidate_row->[$index] eq ''); if ($$next_candidate_row->[$index] < $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] > $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if $$next_candidate_row->[$index] == $_; } return -1; } }; } else { # A string 'in' comparison @$value = sort { $a cmp $b } @$value; return sub { if ($$next_candidate_row->[$index] lt $value->[0]) { return -1; } elsif ($$next_candidate_row->[$index] gt $value->[-1]) { return 1; } else { foreach ( @$value ) { return 0 if $$next_candidate_row->[$index] eq $_; } return -1; } }; } } elsif ($operator eq 'not in') { if (! @$value) { return $ALWAYS_FALSE; } if ($property->is_numeric and $self->_things_in_list_are_numeric($value)) { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); foreach ( @$value ) { return -1 if $$next_candidate_row->[$index] == $_; } return 0; } } else { return sub { foreach ( @$value ) { return -1 if $$next_candidate_row->[$index] eq $_; } return 0; } } } elsif ($operator eq 'like') { # 'like' is always a string comparison. In addition, we can't know if we're ahead # or behind in the file's ID columns, so the only two return values are 0 and 1 return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false # Convert SQL-type wildcards to Perl-type wildcards # Convert a % to a *, and _ to ., unless they're preceeded by \ to escape them. # Not that this isn't precisely correct, as \\% should really mean a literal \ # followed by a wildcard, but we can't be correct in all cases without including # a real parser. This will catch most cases. $value =~ s/(?[$index] eq ''); if ($$next_candidate_row->[$index] =~ $regex) { return 0; } else { return 1; } }; } elsif ($operator eq 'not like') { return $ALWAYS_FALSE if ($value eq ''); # property like NULL is always false $value =~ s/(?[$index] eq ''); if ($$next_candidate_row->[$index] =~ $regex) { return 1; } else { return 0; } }; # FIXME - should we only be testing the numericness of the property? } elsif ($property->is_numeric and $self->_things_in_list_are_numeric([$value])) { # Basic numeric comparisons if ($operator eq '=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number return $$next_candidate_row->[$index] <=> $value; }; } elsif ($operator eq '<') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] < $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] <= $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] > $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] >= $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { $$next_candidate_row->[$index] ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { $$next_candidate_row->[$index] ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { return 0 if ($$next_candidate_row->[$index] eq ''); # null always != a number $$next_candidate_row->[$index] != $value ? 0 : -1; } } } else { # Basic string comparisons if ($operator eq '=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' xor $value eq ''); return $$next_candidate_row->[$index] cmp $value; }; } elsif ($operator eq '<') { return sub { $$next_candidate_row->[$index] lt $value ? 0 : 1; }; } elsif ($operator eq '<=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' or $value eq ''); $$next_candidate_row->[$index] le $value ? 0 : 1; }; } elsif ($operator eq '>') { return sub { $$next_candidate_row->[$index] gt $value ? 0 : -1; }; } elsif ($operator eq '>=') { return sub { return -1 if ($$next_candidate_row->[$index] eq '' or $value eq ''); $$next_candidate_row->[$index] ge $value ? 0 : -1; }; } elsif ($operator eq 'true') { return sub { $$next_candidate_row->[$index] ? 0 : -1; }; } elsif ($operator eq 'false') { return sub { $$next_candidate_row->[$index] ? -1 : 0; }; } elsif ($operator eq '!=' or $operator eq 'ne') { return sub { $$next_candidate_row->[$index] ne $value ? 0 : -1; } } } } sub create_iterator_closure_for_rule { my($self,$rule) = @_; my $class_name = $rule->subject_class_name; my $class_meta = $class_name->__meta__; my $rule_template = $rule->template; my $csv_column_order_names = $self->column_order; my $csv_column_count = scalar @$csv_column_order_names; my $operators_for_properties = $rule_template->operators_for_properties(); my $values_for_properties = $rule->legacy_params_hash; foreach ( values %$values_for_properties ) { if (ref eq 'HASH' and exists $_->{'value'}) { $_ = $_->{'value'}; } } my $sort_order_names = $self->sort_order; my %sort_column_names = map { $_ => 1 } @$sort_order_names; my @non_sort_column_names = grep { ! exists($sort_column_names{$_}) } @$csv_column_order_names; my %column_name_to_index_map; for (my $i = 0; $i < @$csv_column_order_names; $i++) { $column_name_to_index_map{$csv_column_order_names->[$i]} = $i; } # Index in the split-file-data for each sorted column in order my @sort_order_column_indexes = map { $column_name_to_index_map{$_} } @$sort_order_names; my(%property_meta_for_column_name); foreach my $column_name ( @$csv_column_order_names ) { my $prop = UR::Object::Property->get(class_name => $class_name, column_name => $column_name); our %WARNED_ABOUT_COLUMN; unless ( $prop or $WARNED_ABOUT_COLUMN{$class_name . '::' . $column_name}++) { $self->warning_message("Couldn't find a property in class $class_name that goes with column $column_name"); next; } $property_meta_for_column_name{$column_name} = $prop; } my @rule_columns_in_order; # The order we should perform rule matches on - value is the index in @next_file_row to test my @comparison_for_column; # closures to call to perform the match - same order as @rule_columns_in_order my $last_sort_column_in_rule = -1; # Last index in @rule_columns_in_order that applies when trying "the shortcut" my $looking_for_sort_columns = 1; my $next_candidate_row; # This will be filled in by the closure below foreach my $column_name ( @$sort_order_names, @non_sort_column_names ) { my $property_meta = $property_meta_for_column_name{$column_name}; unless ($property_meta) { Carp::croak("Class $class_name has no property connected to column named '$column_name' in data source ".$self->id); } my $property_name = $property_meta->property_name; if (! $operators_for_properties->{$property_name}) { $looking_for_sort_columns = 0; next; } elsif ($looking_for_sort_columns && $sort_column_names{$column_name}) { $last_sort_column_in_rule++; } else { # There's been a gap in the ID column list in the rule, stop looking for # further ID columns $looking_for_sort_columns = 0; } push @rule_columns_in_order, $column_name_to_index_map{$column_name}; my $operator = $operators_for_properties->{$property_name}; my $rule_value = $values_for_properties->{$property_name}; my $comparison_function = $self->_comparator_for_operator_and_property($property_meta, \$next_candidate_row, $column_name_to_index_map{$column_name}, $operator, $rule_value); unless ($comparison_function) { Carp::croak("Unknown operator '$operator' in file data source filter"); } push @comparison_for_column, $comparison_function; } my $split_regex = $self->_regex(); # FIXME - another performance boost might be to do some kind of binary search # against the file to set the initial/next position? my $file_pos = 0; # search in the offset cache for something helpful my $offset_cache = $self->_offset_cache(); # If the rule doesn't touch the sorted columns, then we can't use the offset cache for help :( if ($last_sort_column_in_rule >= 0) { # Starting at index 1 because we're interested in the file and seek data, not if it's in use # offset 0 is the in-use flag, offset 1 is a ref to the file data and offset 2 is the file seek pos SEARCH_CACHE: for (my $i = 1; $i < @$offset_cache; $i+=3) { next unless (defined($offset_cache->[$i]) && defined($offset_cache->[$i+1])); $next_candidate_row = $offset_cache->[$i]; my $matched = 0; COMPARE_VALUES: for (my $c = 0; $c <= $last_sort_column_in_rule; $c++) { my $comparison = $comparison_for_column[$c]->(); next SEARCH_CACHE if $comparison > 0; if ($comparison < 0) { $matched = 1; last COMPARE_VALUES; } } # If we made it this far, then the file data in this slot is earlier in the file # than the data we're looking for. So, if the seek pos data is later than what # we've found yet, use it instead if ($matched and $offset_cache->[$i+1] > $file_pos) { $file_pos = $offset_cache->[$i+1]; } } } my($monitor_start_time,$monitor_printed_first_fetch); if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); $monitor_printed_first_fetch = 0; my @filters_list; for (my $i = 0; $i < @rule_columns_in_order; $i++) { my $column = $rule_columns_in_order[$i]; my $column_name = $csv_column_order_names->[$column]; my $is_sorted = $i <= $last_sort_column_in_rule ? ' (sorted)' : ''; my $operator = $operators_for_properties->{$column_name} || '='; my $rule_value = $values_for_properties->{$column_name}; if (ref $rule_value eq 'ARRAY') { $rule_value = '[' . join(',', @$rule_value) . ']'; } my $filter_string = $column_name . " $operator $rule_value" . $is_sorted; push @filters_list, $filter_string; } my $filter_list = join("\n\t", @filters_list); UR::DBI->sql_fh->printf("\nFILE: %s\nFILTERS %s\n\n", $self->server, $filter_list); } $self->{'_last_read_fingerprint'} ||= ''; my $record_separator = $self->record_separator; my $cache_slot = $self->_allocate_offset_cache_slot(); my $cache_insert_counter = 100; # a "breadcrumb" will be left in the offset cache after this many lines are read my $lines_read = 0; my $printed_first_match = 0; my $lines_matched = 0; my $fh; # File handle we'll be reading from my $read_fingerprint; # The stringified version of $iterator (to avoid circular references), filled in below my $iterator = sub { unless (ref($fh)) { $fh = $self->get_default_handle(); # Lock the file for reading... For more fine-grained locking we could move this to # after READ_LINE_FROM_FILE: but that would slow down read operations a bit. If # there ends up being a problem with lock contention, go ahead and move it before $line = <$fh>; #flock($fh,LOCK_SH); } if ($monitor_start_time && ! $monitor_printed_first_fetch) { UR::DBI->sql_fh->printf("FILE: FIRST FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); $monitor_printed_first_fetch = 1; } if ($self->{'_last_read_fingerprint'} ne $read_fingerprint) { UR::DBI->sql_fh->printf("FILE: Resetting file position to $file_pos\n") if $ENV{'UR_DBI_MONITOR_SQL'}; # The last read was from a different request, reset the position $fh->seek($file_pos,0); if ($file_pos == 0) { my $skip = $self->skip_first_line; while ($skip-- > 0) { scalar(<$fh>); } } $file_pos = $fh->tell(); $self->{'_last_read_fingerprint'} = $read_fingerprint; } local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $line; READ_LINE_FROM_FILE: until($line) { # Hack for OSX 10.5. # At EOF, the getline below will return undef. Most builds of Perl # will also set $! to 0 at EOF so you can distinguish between the cases # of EOF (which may have actually happened a while ago because of buffering) # and an actual read error. OSX 10.5's Perl does not, and so $! # retains whatever value it had after the last failed syscall, likely # a stat() while looking for a Perl module. This should have no effect # other platforms where you can't trust $! at arbitrary points in time # anyway $! = 0; $line = <$fh>; unless (defined $line) { if ($!) { redo READ_LINE_FROM_FILE if ($! == EAGAIN or $! == EINTR); my $pathname = $self->server(); Carp::confess("getline() failed for DataSource $self pathname $pathname boolexpr $rule: $!"); } # at EOF. Close up shop and return #flock($fh,LOCK_UN); $fh = undef; if ($monitor_start_time) { UR::DBI->sql_fh->printf("FILE: at EOF\nFILE: $lines_read lines read for this request. $lines_matched matches\nFILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } return; } $lines_read++; my $last_read_size = length($line); chomp $line; # FIXME - to support record-oriented files, we need some replacement for this... $next_candidate_row = [ split($split_regex, $line, $csv_column_count) ]; $#{$a} = $csv_column_count-1; $file_pos = $fh->tell(); my $file_pos_before_read = $file_pos - $last_read_size; # Every so many lines read, leave a breadcrumb about what we've seen unless ($lines_read % $cache_insert_counter) { $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; $self->_free_offset_cache_slot($cache_slot); # get a new slot $cache_slot = $self->_allocate_offset_cache_slot(); $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; $cache_insert_counter <<= 2; # Double the insert counter } for (my $i = 0; $i < @rule_columns_in_order; $i++) { my $comparison = $comparison_for_column[$i]->(); if ($comparison > 0 and $i <= $last_sort_column_in_rule) { # We've gone past the last thing that could possibly match if ($monitor_start_time) { UR::DBI->sql_fh->printf("FILE: $lines_read lines read for this request. $lines_matched matches\nFILE: TOTAL EXECUTE-FETCH TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } #flock($fh,LOCK_UN); # Save the info from the last row we read $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; return; } elsif ($comparison) { # comparison didn't match, read another line from the file redo READ_LINE_FROM_FILE; } # That comparison worked... stay in the for() loop for other comparisons } # All the comparisons return '0', meaning they passed # Now see if the offset cache file data is different than the row we just read COMPARE_TO_CACHE: foreach my $column ( @sort_order_column_indexes) { no warnings 'uninitialized'; if ($offset_cache->[$cache_slot+1]->[$column] ne $next_candidate_row->[$column]) { # They're different. Update the offset cache data $offset_cache->[$cache_slot+1] = $next_candidate_row; $offset_cache->[$cache_slot+2] = $file_pos_before_read; last COMPARE_TO_CACHE; } } if (! $printed_first_match and $monitor_start_time) { UR::DBI->sql_fh->printf("FILE: First match after reading $lines_read lines\n"); $printed_first_match=1; } $lines_matched++; return $next_candidate_row; } }; # end sub $iterator $read_fingerprint = $iterator . ''; Sub::Name::subname('UR::DataSource::File::__datasource_iterator(closure)__', $iterator); my $count = $self->_open_query_count() || 0; $self->_open_query_count($count+1); bless $iterator, 'UR::DataSource::File::Tracker'; $iterator_data_source{$iterator} = $self; $iterator_cache_slot_refs{$iterator} = \$cache_slot; return $iterator; } sub UR::DataSource::File::Tracker::DESTROY { my $iterator = shift; my $ds = delete $iterator_data_source{$iterator}; return unless $ds; # The data source may have gone out of scope first during global destruction my $cache_slot_ref = delete $iterator_cache_slot_refs{$iterator}; if (defined($cache_slot_ref) and defined($$cache_slot_ref)) { # Mark this slot unused #print STDERR "Freeing cache slot $cache_slot\n"; #$ds->_offset_cache->[$$cache_slot_ref] = 0; $ds->_free_offset_cache_slot($$cache_slot_ref); } my $count = $ds->_open_query_count(); $ds->_open_query_count(--$count); return unless ($ds->quick_disconnect); if ($count == 0) { # All open queries have supposedly been fulfilled. Close the # file handle and undef it so get_default_handle() will re-open if necessary my $fh = $ds->{'_fh'}; UR::DBI->sql_fh->printf("FILE: CLOSING fileno ".fileno($fh)."\n") if ($ENV{'UR_DBI_MONITOR_SQL'}); #flock($fh,LOCK_UN); $fh->close(); $ds->{'_fh'} = undef; } } # Names of creation params that we should force to be listrefs our %creation_param_is_list = map { $_ => 1 } qw( column_order file_list sort_order constant_values ); sub create_from_inline_class_data { my($class, $class_data, $ds_data) = @_; # User didn't specify columns in the file. Assumme every property is a column, and in the same order unless (exists $ds_data->{'column_order'}) { Carp::croak "data_source has no column_order specified"; } $ds_data->{'server'} ||= $ds_data->{'path'} || $ds_data->{'file'}; my %ds_creation_params; foreach my $param ( qw( delimiter record_separator column_order skip_first_line server file_list sort_order constant_values ) ) { if (exists $ds_data->{$param}) { if ($creation_param_is_list{$param} and ref($ds_data->{$param}) ne 'ARRAY') { $ds_creation_params{$param} = \( $ds_data->{$param} ); } else { $ds_creation_params{$param} = $ds_data->{$param}; } } } my($namespace, $class_name) = ($class_data->{'class_name'} =~ m/^(\w+?)::(.*)/); my $ds_id = "${namespace}::DataSource::${class_name}"; my $ds_type = delete $ds_data->{'is'}; my $ds = $ds_type->create( %ds_creation_params, id => $ds_id ); return $ds; } # The string used to join fields of a row together # # Since the 'delimiter' property is interpreted as a regex in the reading # code, we'll try to be smart about making a real string from that. # # subclasses can override this to provide a different implementation sub join_pattern { my $self = shift; my $join_pattern = $self->delimiter; # make some common substitutions... if ($join_pattern eq '\s*,\s*') { # The default... return ', '; } $join_pattern =~ s/\\s*//g; # Turn 0-or-more whitespaces to nothing $join_pattern =~ s/\\t/\t/; # tab $join_pattern =~ s/\\s/ /; # whitespace return $join_pattern; } sub _sync_database { my $self = shift; my %params = @_; unless (ref($self)) { if ($self->isa("UR::Singleton")) { $self = $self->_singleton_object; } else { die "Called as a class-method on a non-singleton datasource!"; } } my $read_fh = $self->get_default_handle(); unless ($read_fh) { Carp::croak($self->class . ": Can't _sync_database(): Can't open file " . $self->server . " for reading: $!"); } my $original_data_file = $self->server; my $original_data_dir = File::Basename::dirname($original_data_file); my $use_quick_rename; unless (-d $original_data_dir){ File::Path::mkpath($original_data_dir); } if (-w $original_data_dir) { $use_quick_rename = 1; # We can write to the data dir } elsif (! -w $original_data_file) { $self->error_message("Neither the directory nor the file for $original_data_file are writable - cannot sync_database"); return; } my $split_regex = $self->_regex(); my $join_pattern = $self->join_pattern; my $record_separator = $self->record_separator; local $/; # Make sure some wise guy hasn't changed this out from under us $/ = $record_separator; my $csv_column_order_names = $self->column_order; my $csv_column_count = scalar(@$csv_column_order_names); my %column_name_to_index_map; for (my $i = 0; $i < @$csv_column_order_names; $i++) { $column_name_to_index_map{$csv_column_order_names->[$i]} = $i; } my $changed_objects = delete $params{changed_objects}; # We're going to assumme all the passed-in objects are of the same class *gulp* my $class_name = $changed_objects->[0]->class; my $class_meta = UR::Object::Type->get(class_name => $class_name); my %column_name_to_property_meta = map { $_->column_name => $_ } grep { $_->column_name } $class_meta->all_property_metas; my @property_names_in_column_order; foreach my $column_name ( @$csv_column_order_names ) { my $prop_meta = $column_name_to_property_meta{$column_name}; unless ($prop_meta) { die "Data source " . $self->class . " id " . $self->id . " could not resolve a $class_name property for the data source's column named $column_name"; } push @property_names_in_column_order, $prop_meta->property_name; } my $insert = []; my $update = {}; my $delete = {}; foreach my $obj ( @$changed_objects ) { if ($obj->isa('UR::Object::Ghost')) { # This should be removed from the file my $original = $obj->{'db_committed'}; my $line = join($join_pattern, @{$original}{@property_names_in_column_order}) . $record_separator; $delete->{$line} = $obj; } elsif ($obj->{'db_committed'}) { # This object is changed since it was read in the file my $original = $obj->{'db_committed'}; my $original_line = join($join_pattern, @{$original}{@property_names_in_column_order}) . $record_separator; my $changed_line = join($join_pattern, @{$obj}{@property_names_in_column_order}) . $record_separator; $update->{$original_line} = $changed_line; } else { # This object is new and should be added to the file push @$insert, [ @{$obj}{@property_names_in_column_order} ]; } } my $sort_order_names = $self->sort_order; foreach my $sort_column_name ( @$sort_order_names ) { unless (exists $column_name_to_index_map{$sort_column_name}) { Carp::croak("Column name '$sort_column_name' appears in the sort_order list, but not in the column_order list for data source ".$self->id); } } my $file_is_sorted = scalar(@$sort_order_names); my %column_sorts_numerically = map { $_->column_name => $_->is_numeric } values %column_name_to_property_meta; my $row_sort_sub = sub ($$) { my $comparison; foreach my $column_name ( @$sort_order_names ) { my $i = $column_name_to_index_map{$column_name}; if ($column_sorts_numerically{$column_name}) { $comparison = $_[0]->[$i] <=> $_[1]->[$i]; } else { $comparison = $_[0]->[$i] cmp $_[1]->[$i]; } return $comparison if $comparison != 0; } return 0; }; if ($sort_order_names && $file_is_sorted && scalar(@$insert)) { # the inserted things should be sorted the same way as the file my @sorted = sort $row_sort_sub @$insert; $insert = \@sorted; } my $write_fh; my $temp_file_name; if ($use_quick_rename) { $temp_file_name = sprintf("%s/.%d.%d" , $original_data_dir, time(), $$); $write_fh = IO::File->new($temp_file_name, O_WRONLY|O_CREAT); } else { $write_fh = File::Temp->new(UNLINK => 1); $temp_file_name = $write_fh->filename if ($write_fh); } unless ($write_fh) { Carp::croak "Can't create temporary file for writing: $!"; } my $monitor_start_time; if ($ENV{'UR_DBI_MONITOR_SQL'}) { $monitor_start_time = Time::HiRes::time(); my $time = time(); UR::DBI->sql_fh->printf("\nFILE: SYNC_DATABASE AT %d [%s]. Started transaction for %s to temp file %s\n", $time, scalar(localtime($time)), $original_data_file, $temp_file_name); } unless (flock($read_fh,LOCK_SH)) { unless ($! == EOPNOTSUPP ) { Carp::croak($self->class(). ": Can't get exclusive lock for file ".$self->server.": $!"); } } # write headers to the new file for (my $i = 0; $i < $self->skip_first_line; $i++) { my $line = <$read_fh>; $write_fh->print($line); } my $line; READ_A_LINE: while(1) { unless ($line) { $line = <$read_fh>; last unless defined $line; } if ($file_is_sorted && scalar(@$insert)) { # there are sorted things waiting to insert my $chomped = $line; chomp $chomped; my $row = [ split($split_regex, $chomped, $csv_column_count) ]; my $comparison = $row_sort_sub->($row, $insert->[0]); if ($comparison > 0) { # write the object's data no warnings 'uninitialized'; # Some of the object's data may be undef my $new_row = shift @$insert; my $new_line = join($join_pattern, @$new_row) . $record_separator; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("INSERT >>$new_line<<\n"); } $write_fh->print($new_line); # Don't undef the last line read, meaning it could still be written to the output... next READ_A_LINE; } } if (my $obj = delete $delete->{$line}) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("DELETE >>$line<<\n"); } $line = undef; next; } elsif (my $changed = delete $update->{$line}) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("UPDATE replace >>$line<< with >>$changed<<\n"); } $write_fh->print($changed); $line = undef; next; } else { # This line from the file was unchanged in the app $write_fh->print($line); $line = undef; } } if (keys %$delete) { $self->warning_message("There were ",scalar(keys %$delete)," deleted $class_name objects that did not match data in the file"); } if (keys %$update) { $self->warning_message("There were ",scalar(keys %$update)," updated $class_name objects that did not match data in the file"); } # finish out by writing the rest of the new data foreach my $new_row ( @$insert ) { no warnings 'uninitialized'; # Some of the object's data may be undef my $new_line = join($join_pattern, @$new_row) . $record_separator; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("INSERT >>$new_line<<\n"); } $write_fh->print($new_line); } $write_fh->close(); if ($use_quick_rename) { if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("FILE: COMMIT rename $temp_file_name over $original_data_file\n"); } unless(rename($temp_file_name, $original_data_file)) { $self->error_message("Can't rename the temp file over the original file: $!"); return; } } else { # We have to copy the data from the temp file to the original file if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->print("FILE: COMMIT write over $original_data_file in place\n"); } my $new_write_fh = IO::File->new($original_data_file, O_WRONLY|O_TRUNC); unless ($new_write_fh) { $self->error_message("Can't open $original_data_file for writing: $!"); return; } my $temp_file_fh = IO::File->new($temp_file_name); unless ($temp_file_fh) { $self->error_message("Can't open $temp_file_name for reading: $!"); return; } while(<$temp_file_fh>) { $new_write_fh->print($_); } $new_write_fh->close(); } # Because of the rename/copy process during syncing, the previously opened filehandle may # not be valid anymore. get_default_handle will reopen the file next time it's needed $self->_invalidate_cache(); $self->{_fh} = undef; if ($ENV{'UR_DBI_MONITOR_SQL'}) { UR::DBI->sql_fh->printf("FILE: TOTAL COMMIT TIME: %.4f s\n", Time::HiRes::time() - $monitor_start_time); } flock($read_fh, LOCK_UN); $read_fh->close(); # FIXME - this is ugly... With RDBMS-type data sources, they will call $dbh->commit() which # gets to UR::DBI->commit(), which calls _set_object_saved_committed for them. Since we're # not using DBI we have to do this 2-part thing ourselves. In the future, we might break # out things so the saving to the temp file goes in _sync_database(), and moving the temp # file over the original goes in commit() unless ($self->_set_specified_objects_saved_uncommitted($changed_objects)) { Carp::croak("Error setting objects to a saved state after sync_database. Exiting."); return; } $self->_set_specified_objects_saved_committed($changed_objects); return 1; } sub initializer_should_create_column_name_for_class_properties { 1; } 1; =pod =head1 NAME UR::DataSource::File - Parent class for file-based data sources =head1 DEPRECATED This module is deprecated. Use UR::DataSource::Filesystem instead. =head1 SYNOPSIS package MyNamespace::DataSource::MyFile; class MyNamespace::DataSource::MyFile { is => ['UR::DataSource::File', 'UR::Singleton'], }; sub server { '/path/to/file' } sub delimiter { "\t" } sub column_order { ['thing_id', 'thing_name', 'thing_color' ] } sub sort_order { ['thing_id'] } package main; class MyNamespace::Thing { id_by => 'thing_id', has => [ 'thing_id', 'thing_name', 'thing_color' ], data_source => 'MyNamespace::DataSource::MyFile', } my @objs = MyNamespace::Thing->get(thing_name => 'Bob'); =head1 DESCRIPTION Classes which wish to retrieve their data from a regular file can use a UR::DataSource::File-based data source. The modules implementing these data sources live under the DataSource subdirectory of the application's Namespace, by convention. Besides defining a class for your data source inheriting from UR::DataSource::File, it should have the following methods, either as properties or functions in the package. =head2 Configuration These methods determine the configuration for your data source. =over 4 =item server() server() should return a string representing the pathname of the file where the data is stored. =item file_list() The file_list() method should return a listref of pathnames to one or more identical files where data is stored. Use file_list() instead of server() when you want to load-balance several NFS servers, for example. You must have either server() or file_list() in your module, but not both. The existence of server() takes precedence over file_list(). =item delimiter() delimiter() should return a string representing how the fields in each record are split into columns. This string is interpreted as a regex internally. The default delimiter is "\s*,\s*" meaning that the file is separated by commas. =item record_separator() record_separator() should return a string that gets stored in $/ before getline() is called on the file's filehandle. The default record_separator() is "\n" meaning that the file's records are separated by newlines. =item skip_first_line() skip_first_line() should return a boolean value. If true, the first line of the file is ignored, for example if the first line defines the columns in the file. =item column_order() column_order() should return a listref of column names in the file. column_order is required; there is no default. =item sort_order() If the data file is sorted in some way, sort_order() should return a listref of column names (which must exist in column_order()) by which the file is sorted. This gives the system a hint about how the file is structured, and is able to make shortcuts when reading the file to speed up data access. The default is to assumme the file is not sorted. =back =head1 INHERITANCE UR::DataSource =head1 SEE ALSO UR, UR::DataSource =cut SQLite.pm000444023532023421 7417712121654175 16507 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::SQLite; use strict; use warnings; =pod =head1 NAME UR::DataSource::SQLite - base class for datasources using the SQLite3 RDBMS =head1 SYNOPSIS In the shell: ur define datasource sqlite Or write the singleton to represent the source directly: class Acme::DataSource::MyDB1 { is => 'UR::DataSource::SQLite', has_constant => [ _database_file_path => '/var/lib/acme-app/mydb1.sqlitedb' ] }; =cut require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::SQLite', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "SQLite" } sub default_owner { return 'main'; } sub owner { default_owner() } sub login { undef } sub auth { undef } sub create_dbh { my $self = shift->_singleton_object(); $self->_init_database; return $self->SUPER::create_dbh(@_); } sub database_exists { my $self = shift; return 1 if -e $self->server; return 1 if -e $self->_data_dump_path; # exists virtually, and will dynamicaly instantiate return; } sub create_database { my $self = shift; die "Database exists!" if $self->database_exists; my $path = $self->server; return 1 if IO::File->new(">$path"); } sub can_savepoint { 0;} # Dosen't support savepoints # SQLite API sub _schema_path { return shift->_database_file_path() . '-schema'; } sub _data_dump_path { return shift->_database_file_path() . '-dump'; } # FIXME is there a way to make this an object parameter instead of a method sub server { my $self = shift->_singleton_object(); my $path = $self->__meta__->module_path; my $ext = $self->_extension_for_db; $path =~ s/\.pm$/$ext/ or Carp::croak("Odd module path $path. Expected something endining in '.pm'"); my $dir = File::Basename::dirname($path); return $path; } *_database_file_path = \&server; sub _extension_for_db { '.sqlite3'; } sub _journal_file_path { my $self = shift->_singleton_object(); return $self->server . "-journal"; } sub _init_database { my $self = shift->_singleton_object(); my $db_file = $self->server; my $dump_file = $self->_data_dump_path; my $schema_file = $self->_schema_path; my $db_time = (stat($db_file))[9]; my $dump_time = (stat($dump_file))[9]; my $schema_time = (stat($schema_file))[9]; if ($schema_time && ((-e $db_file and $schema_time > $db_time) or (-e $dump_file and $schema_time > $dump_time))) { $self->warning_message("Schema file is newer than the db file or the dump file. Replacing db_file $db_file."); my $dbbak_file = $db_file . '-bak'; my $dumpbak_file = $dump_file . '-bak'; unlink $dbbak_file if -e $dbbak_file; unlink $dumpbak_file if -e $dumpbak_file; rename $db_file, $dbbak_file if -e $db_file; rename $dump_file, $dumpbak_file if -e $dump_file; if (-e $db_file) { Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!"; } if (-e $dump_file) { Carp::croak "Failed to move out-of-date file $dump_file out of the way for reconstruction! $!"; } } if (-e $db_file) { if ($dump_time && ($db_time < $dump_time)) { my $bak_file = $db_file . '-bak'; $self->warning_message("Dump file is newer than the db file. Replacing db_file $db_file."); unlink $bak_file if -e $bak_file; rename $db_file, $bak_file; if (-e $db_file) { Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!"; } } } # NOTE: don't make this an "else", since we might go into both branches because we delete the file above. unless (-e $db_file) { # initialize a new database from the one in the base class # should this be moved to connect time? # TODO: auto re-create things as needed based on timestamp if (-e $dump_file) { # create from dump $self->warning_message("Re-creating $db_file from $dump_file."); $self->_load_db_from_dump_internal($dump_file); unless (-e $db_file) { Carp::croak("Failed to import $dump_file into $db_file!"); } } elsif ( (not -e $db_file) and (-e $schema_file) ) { # create from schema $self->warning_message("Re-creating $db_file from $schema_file."); $self->_load_db_from_dump_internal($schema_file); unless (-e $db_file) { Carp::croak("Failed to import $dump_file into $db_file!"); } } elsif ($self->class ne __PACKAGE__) { # copy from the parent class (disabled) Carp::croak("No schema or dump file found for $db_file.\n Tried schema path $schema_file\n and dump path $dump_file\nIf you still have *sqlite3n* SQLite database files please rename them to *sqlite3*, without the 'n'"); my $template_database_file = $self->SUPER::server(); unless (-e $template_database_file) { Carp::croak("Missing template database file: $db_file! Cannot initialize database for " . $self->class); } unless(File::Copy::copy($template_database_file,$db_file)) { Carp::croak("Error copying $db_file to $template_database_file to initialize database!"); } unless(-e $db_file) { Carp::croak("File $db_file not found after copy from $template_database_file. Cannot initialize database!"); } } else { Carp::croak("No db file found, and no dump or schema file found from which to re-construct a db file!"); } } return 1; } sub _init_created_dbh { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; # wait one minute busy timeout $dbh->func(1800000,'busy_timeout'); return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(sqlite|\$|URMETA)/; } sub _get_sequence_name_for_table_and_column { my $self = shift->_singleton_object; my ($table_name,$column_name) = @_; my $dbh = $self->get_default_handle(); # See if the sequence generator "table" is already there my $seq_table = sprintf('URMETA_%s_%s_seq', $table_name, $column_name); unless ($self->{'_has_sequence_generator'}->{$seq_table} or grep {$_ eq $seq_table} $self->get_table_names() ) { unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTOINCREMENT)")) { die "Failed to create sequence generator $seq_table: ".$dbh->errstr(); } } $self->{'_has_sequence_generator'}->{$seq_table} = 1; return $seq_table; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; my $dbh = $self->get_default_handle(); # FIXME can we use a statement handle with a wildcard as the table name here? unless ($dbh->do("INSERT into $sequence_name values(null)")) { die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr; } my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value'); unless (defined $new_id) { die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr; } unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) { die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration"; } return $new_id; } # Overriding this so we can force the schema to 'main' for older versions of SQLite # # NOTE: table_info (called by SUPER::get_table_details_from_data_dictionary) in older # versions of DBD::SQLite does not return data for tables in other attached databases. # # This probably isn't an issue... Due to the limited number of people using older DBD::SQLite # (of particular note is that OSX 10.5 and earlier use such an old version), interseted with # the limited number of people using attached databases, it's probably not a problem. # The commit_between_schemas test does do this. If it turns out it is a problem, we could # appropriate the code from recent DBD::SQLite::table_info sub get_table_details_from_data_dictionary { my $self = shift; my $sth = $self->SUPER::get_table_details_from_data_dictionary(@_); if ($DBD::SQLite::VERSION >= 1.26_04 || !$sth) { return $sth; } my($catalog,$schema,$table_name) = @_; my @tables; my @returned_names; while (my $info = $sth->fetchrow_hashref()) { #@returned_names ||= (keys %$info); unless (@returned_names) { @returned_names = keys(%$info); } $info->{'TABLE_SCHEM'} ||= 'main'; push @tables, $info; } my $dbh = $self->get_default_handle(); my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); unless (@returned_names) { @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS ); } my $returned_sth = $sponge->prepare("table_info $table_name", { rows => [ map { [ @{$_}{@returned_names} ] } @tables ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # DBD::SQLite doesn't implement column_info. This is the UR::DataSource version of the same thing sub get_column_details_from_data_dictionary { my($self,$catalog,$schema,$table,$column) = @_; my $dbh = $self->get_default_handle(); # Convert the SQL wildcards to regex wildcards $column = '' unless defined $column; $column =~ s/%/.*/; $column =~ s/_/./; my $column_regex = qr(^$column$); my $sth_tables = $dbh->table_info($catalog, $schema, $table, 'TABLE'); my @table_names = map { $_->{'TABLE_NAME'} } @{ $sth_tables->fetchall_arrayref({}) }; my $override_owner; if ($DBD::SQLite::VERSION < 1.26_04) { $override_owner = 'main'; } my @columns; foreach my $table_name ( @table_names ) { my $sth = $dbh->prepare("PRAGMA table_info($table_name)") or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); $sth->execute() or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); while (my $info = $sth->fetchrow_hashref()) { next unless $info->{'name'} =~ m/$column_regex/; # SQLite doesn't parse our that type varchar(255) actually means type varchar size 255 my $data_type = $info->{'type'}; my $column_size; if ($data_type =~ m/(\S+)\s*\((\S+)\)/) { $data_type = $1; $column_size = $2; } my $node = {}; $node->{'TABLE_CAT'} = $catalog; $node->{'TABLE_SCHEM'} = $schema || $override_owner; $node->{'TABLE_NAME'} = $table_name; $node->{'COLUMN_NAME'} = $info->{'name'}; $node->{'DATA_TYPE'} = $data_type; $node->{'TYPE_NAME'} = $data_type; $node->{'COLUMN_SIZE'} = $column_size; $node->{'NULLABLE'} = ! $info->{'notnull'}; $node->{'IS_NULLABLE'} = ($node->{'NULLABLE'} ? 'YES' : 'NO'); $node->{'REMARKS'} = ""; $node->{'SQL_DATA_TYPE'} = ""; # FIXME shouldn't this be something related to DATA_TYPE $node->{'SQL_DATETIME_SUB'} = ""; $node->{'CHAR_OCTET_LENGTH'} = undef; # FIXME this should be the same as column_size, right? $node->{'ORDINAL_POSITION'} = $info->{'cid'}; $node->{'COLUMN_DEF'} = $info->{'dflt_value'}; # Remove starting and ending 's that appear erroneously with string default values $node->{'COLUMN_DEF'} =~ s/^'|'$//g if defined ( $node->{'COLUMN_DEF'}); push @columns, $node; } } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE ); my $returned_sth = $sponge->prepare("column_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @columns ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } # SQLite doesn't store the name of a foreign key constraint in its metadata directly. # We can guess at it from the SQL used in the table creation. These regexes are probably # sloppy. We could replace them if there were a good SQL parser. sub _resolve_fk_name { my($self, $table_name, $column_list, $r_table_name, $r_column_list) = @_; if (@$column_list != @$r_column_list) { Carp::confess('There are '.scalar(@$column_list).' pk columns and '.scalar(@$r_column_list).' fk columns'); } my($table_info) = $self->_get_info_from_sqlite_master($table_name, 'table'); return unless $table_info; my $col_str = $table_info->{'sql'}; $col_str =~ s/^\s+|\s+$//g; # Remove leading and trailing whitespace $col_str =~ s/\s{2,}/ /g; # Remove multiple spaces if ($col_str =~ m/^CREATE TABLE (\w+)\s*?\((.*?)\)$/is) { unless ($1 eq $table_name) { Carp::croak("Table creation SQL for $table_name is inconsistent. Didn't find table name '$table_name' in string '$col_str'. Found $1 instead."); } $col_str = $2; } else { Carp::croak("Couldn't parse SQL for $table_name"); } my $fk_name; if (@$column_list > 1) { # Multiple column FKs must be specified as a table-wide constraint, and has a well-known format my $fk_list = '\s*' . join('\s*,\s*', @$column_list) . '\s*'; my $uk_list = '\s*' . join('\s*,\s*', @$r_column_list) . '\s*'; my $expected_to_find = sprintf('FOREIGN KEY\s*\(%s\) REFERENCES %s\s*\(%s\)', $fk_list, $r_table_name, $uk_list); my $regex = qr($expected_to_find)i; if ($col_str =~ m/$regex/) { ($fk_name) = ($col_str =~ m/CONSTRAINT (\w+) FOREIGN KEY\s*\($fk_list\)/i); } else { # Didn't find anything... return; } } else { # single-column FK constraints can be specified a couple of ways... # First, try as a table-wide constraint my $col = $column_list->[0]; my $r_col = $r_column_list->[0]; if ($col_str =~ m/FOREIGN KEY\s*\($col\)\s*REFERENCES $r_table_name\s*\($r_col\)/i) { ($fk_name) = ($col_str =~ m/CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\($col\)/i); } else { while ($col_str) { # Try parsing each of the column definitions # commas can't appear in here except to separate each column, right? my $this_col; if ($col_str =~ m/^(.*?)\s*,\s*(.*)/) { $this_col = $1; $col_str = $2; } else { $this_col = $col_str; $col_str = ''; } my($col_name, $col_type) = ($this_col =~ m/^(\w+) (\w+)/); next unless ($col_name and $col_name eq $col); if ($this_col =~ m/REFERENCES $r_table_name\s*\($r_col\)/i) { # It's the right column, and there's a FK constraint on it # Did the FK get a name? ($fk_name) = ($this_col =~ m/CONSTRAINT (\w+) REFERENCES/i); last; } else { # It's the right column, but there's no FK return; } } } } # The constraint didn't have a name. Make up something that'll likely be unique $fk_name ||= join('_', $table_name, @$column_list, $r_table_name, @$r_column_list, 'fk'); return $fk_name; } # We'll only support specifying $fk_table or $pk_table but not both # $fk_table refers to the table where the fk is attached # $pk_table refers to the table the pk points to - where the primary key exists sub get_foreign_key_details_from_data_dictionary { my($self,$fk_catalog,$fk_schema,$fk_table,$pk_catalog,$pk_schema,$pk_table) = @_; my $dbh = $self->get_default_handle(); # first, build a data structure to collect columns of the same foreign key together my %fk_info; if ($fk_table) { my $fksth = $dbh->prepare_cached("PRAGMA foreign_key_list($fk_table)") or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); unless ($fksth->execute()) { $self->error_message("foreign_key_list execute failed: $DBI::errstr"); return; } #my($id, $seq, $to_table, $from, $to); # This will generate an error message when there are no result rows #$fksth->bind_columns(\$id, \$seq, \$to_table, \$from, \$to); while (my $row = $fksth->fetchrow_arrayref) { my($id, $seq, $to_table, $from, $to) = @$row; $fk_info{$id} ||= []; $fk_info{$id}->[$seq] = { from_table => $fk_table, to_table => $to_table, from => $from, to => $to }; } } elsif ($pk_table) { # We'll have to loop through each table in the DB and find FKs that reference # the named table my @tables = $self->_get_info_from_sqlite_master(undef,'table'); my $id = 0; foreach my $table_data ( @tables ) { my $from_table = $table_data->{'table_name'}; $id++; my $fksth = $dbh->prepare_cached("PRAGMA foreign_key_list($from_table)") or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); unless ($fksth->execute()) { $self->error_message("foreign_key_list execute failed: $DBI::errstr"); return; } #my($id, $seq, $to_table, $from, $to); #$fksth->bind_columns(\$id, \$seq, \$to_table, \$from, \$to); while (my $row = $fksth->fetchrow_arrayref) { my(undef, $seq, $to_table, $from, $to) = @$row; next unless $to_table eq $pk_table; # Only interested in fks pointing to $pk_table $fk_info{$id} ||= []; $fk_info{$id}->[$seq] = { from_table => $from_table, to_table => $to_table, from => $from, to => $to }; } } } else { Carp::croak("Can't get_foreign_key_details_from_data_dictionary(): either pk_table ($pk_table) or fk_table ($fk_table) are required"); } # next, format it to get returned as a sth my @ret_data; foreach my $fk_info ( values %fk_info ) { my @column_list = map { $_->{'from'} } @$fk_info; my @r_column_list = map { $_->{'to'} } @$fk_info; my $fk_name = $self->_resolve_fk_name($fk_info->[0]->{'from_table'}, \@column_list, $fk_info->[0]->{'to_table'}, # They'll all have the same table, right? \@r_column_list); foreach my $fk_info_col (@$fk_info) { my $node; $node->{'FK_NAME'} = $fk_name; $node->{'FK_TABLE_NAME'} = $fk_info_col->{'from_table'}; $node->{'FK_COLUMN_NAME'} = $fk_info_col->{'from'}; $node->{'UK_TABLE_NAME'} = $fk_info_col->{'to_table'}; $node->{'UK_COLUMN_NAME'} = $fk_info_col->{'to'}; push @ret_data, $node; } } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( FK_NAME UK_TABLE_NAME UK_COLUMN_NAME FK_TABLE_NAME FK_COLUMN_NAME ); my $table = $pk_table || $fk_table; my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @ret_data ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } sub get_bitmap_index_details_from_data_dictionary { # SQLite dosen't support bitmap indicies, so there aren't any return []; } sub get_unique_index_details_from_data_dictionary { my($self,$table_name) = @_; my $dbh = $self->get_default_handle(); return undef unless $dbh; # First, do a pass looking for unique indexes my $idx_sth = $dbh->prepare(qq(PRAGMA index_list($table_name))); return undef unless $idx_sth; $idx_sth->execute(); my $ret = {}; while(my $data = $idx_sth->fetchrow_hashref()) { next unless ($data->{'unique'}); my $idx_name = $data->{'name'}; my $idx_item_sth = $dbh->prepare(qq(PRAGMA index_info($idx_name))); $idx_item_sth->execute(); while(my $index_item = $idx_item_sth->fetchrow_hashref()) { $ret->{$idx_name} ||= []; push( @{$ret->{$idx_name}}, $index_item->{'name'}); } } return $ret; } # By default, make a text dump of the database at commit time. # This should really be a datasource property sub dump_on_commit { 0; } # We're overriding commit from UR::DS::commit() to add the behavior that after # the actual commit happens, we also make a dump of the database in text format # so that can be version controlled sub commit { my $self = shift; my $has_no_pending_trans = (!-f $self->_journal_file_path()); my $worked = $self->SUPER::commit(@_); return unless $worked; my $db_filename = $self->server(); my $dump_filename = $self->_data_dump_path(); return 1 if ($has_no_pending_trans); return 1 unless $self->dump_on_commit or -e $dump_filename; return $self->_dump_db_to_file_internal(); } # Get info out of the sqlite_master table. Returns a hashref keyed by 'name' # columns are: # type - 'table' or 'index' # name - Name of the object # table_name - name of the table this object references. For tables, it's the same as name, # for indexes, it's the name of the table it's indexing # rootpage - Used internally by sqlite # sql - The sql used to create the thing sub _get_info_from_sqlite_master { my($self, $name,$type) = @_; my(@where, @exec_values); if ($name) { push @where, 'name = ?'; push @exec_values, $name; } if ($type) { push @where, 'type = ?'; push @exec_values, $type; } my $sql = 'select * from sqlite_master'; if (@where) { $sql .= ' where '.join(' and ', @where); } my $dbh = $self->get_default_handle(); my $sth = $dbh->prepare($sql); unless ($sth) { no warnings; $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr); return; } unless ($sth->execute(@exec_values)) { no warnings; $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr); return; } my @rows; while (my $row = $sth->fetchrow_arrayref()) { my $item; @$item{'type','name','table_name','rootpage','sql'} = @$row; # Force all names to lower case so we can find them later push @rows, $item; } return @rows; } # This is used if, for whatever reason, we can't sue the sqlite3 command-line # program to load up the database. We'll make a good-faith effort to parse # the SQL text, but it won't be fancy. This is intended to be used to initialize # meta DB dumps, so we should have to worry about escaping quotes, multi-line # statements, etc. # # The real DB file should be moved out of the way before this is called. The existing # DB file will be removed. sub _load_db_from_dump_internal { my $self = shift; my $file_name = shift; my $fh = IO::File->new($file_name); unless ($fh) { Carp::croak("Can't open DB dump file $file_name: $!"); } my $db_file = $self->server; if (-f $db_file) { unless(unlink($db_file)) { Carp::croak("Can't remove DB file $db_file: $!"); } } my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file",'','',{ AutoCommit => 0, RaiseError => 0 }); unless($dbh) { Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr"); } my $dump_file_contents = do { local( $/ ) ; <$fh> }; my @sql = split(';',$dump_file_contents); for (my $i = 0; $i < @sql; $i++) { my $sql = $sql[$i]; next unless ($sql =~ m/\S/); # Skip blank lines next if ($sql =~ m/BEGIN TRANSACTION|COMMIT/i); # We're probably already in a transaction # Is it restoring the foreign_keys setting? if ($sql =~ m/PRAGMA foreign_keys\s*=\s*(\w+)/) { my $value = $1; my $fk_setting = $self->_get_foreign_key_setting(); if (! defined($fk_setting)) { # This version of SQLite cannot enforce foreign keys. # Print a warning message if they're trying to turn it on. # also, remember the setting so we can preserve its value # in _dump_db_to_file_internal() $self->_cache_foreign_key_setting_from_file($value); if ($value ne 'OFF') { $self->warning_message("Data source ".$self->id." does not support foreign key enforcement, but the dump file $db_file attempts to turn it on"); } next; } } unless ($dbh->do($sql)) { Carp::croak("Error processing SQL statement $i from DB dump file:\n$sql\nDBI error was: $DBI::errstr\n"); } } $dbh->commit(); $dbh->disconnect(); return 1; } sub _cache_foreign_key_setting_from_file { my $self = shift; our %foreign_key_setting_from_file; my $id = $self->id; if (@_) { $foreign_key_setting_from_file{$id} = shift; } return $foreign_key_setting_from_file{$id}; } # Is foreign key enforcement on or off? # returns undef if this version of SQLite cannot enforce foreign keys sub _get_foreign_key_setting { my $self = shift; my $id = $self->id; our %foreign_key_setting; unless (exists $foreign_key_setting{$id}) { my $dbh = $self->get_default_handle; my @row = $dbh->selectrow_array('PRAGMA foreign_keys'); $foreign_key_setting{$id} = $row[0]; } return $foreign_key_setting{$id}; } sub resolve_order_by_clause { my($self,$order_by_columns,$order_by_column_data) = @_; my @cols = @$order_by_columns; foreach my $col ( @cols) { my $is_descending; if ($col =~ m/^(-|\+)(.*)$/) { $col = $2; if ($1 eq '-') { $is_descending = 1; } } my $property_meta = $order_by_column_data->{$col} ? $order_by_column_data->{$col}->[1] : undef; my $is_optional; $is_optional = $property_meta->is_optional if $property_meta; if ($is_optional) { if ($is_descending) { $col = "CASE WHEN $col ISNULL THEN 0 ELSE 1 END, $col DESC"; } else { $col = "CASE WHEN $col ISNULL THEN 1 ELSE 0 END, $col"; } } elsif ($is_descending) { $col = $col . ' DESC'; } } return 'order by ' . join(', ',@cols); } sub _dump_db_to_file_internal { my $self = shift; my $fk_setting = $self->_get_foreign_key_setting(); my $file_name = $self->_data_dump_path(); unless (-w $file_name) { # dump file isn't writable... return 1; } my $fh = IO::File->new($file_name, '>'); unless ($fh) { Carp::croak("Can't open DB dump file $file_name for writing: $!"); } my $db_file = $self->server; my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file",'','',{ AutoCommit => 0, RaiseError => 0 }); unless ($dbh) { Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr"); } if (defined $fk_setting) { # Save the value of the foreign_keys setting, if it's supported $fh->print('PRAGMA foreign_keys = ' . ( $fk_setting ? 'ON' : 'OFF' ) .";\n"); } else { # If not supported, but if _load_db_from_dump_internal came across the value, preserve it $fk_setting = $self->_cache_foreign_key_setting_from_file; if (defined $fk_setting) { $fh->print("PRAGMA foreign_keys = $fk_setting;\n"); } } $fh->print("BEGIN TRANSACTION;\n"); my @tables = $self->_get_table_names_from_data_dictionary(); foreach my $table ( @tables ) { my($item_info) = $self->_get_info_from_sqlite_master($table); my $creation_sql = $item_info->{'sql'}; $creation_sql .= ";" unless(substr($creation_sql, -1, 1) eq ";"); $creation_sql .= "\n" unless(substr($creation_sql, -1, 1) eq "\n"); $fh->print($creation_sql); if ($item_info->{'type'} eq 'table') { my $sth = $dbh->prepare("select * from $table"); unless ($sth) { Carp::croak("Can't retrieve data from table $table: $DBI::errstr"); } unless($sth->execute()) { Carp::croak("execute() failed while retrieving data for table $table: $DBI::errstr"); } while(my @row = $sth->fetchrow_array) { foreach my $col ( @row ) { if (! defined $col) { $col = 'null'; } elsif ($col =~ m/\D/) { $col = "'" . $col . "'"; # Put quotes around non-numeric stuff } } $fh->printf("INSERT INTO \"%s\" VALUES(%s);\n", $table, join(',', @row)); } } } $fh->print("COMMIT;\n"); $fh->close(); $dbh->disconnect(); return 1; } 1; Meta.sqlite3-schema000444023532023421 434012121654175 20403 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceCREATE TABLE IF NOT EXISTS dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE IF NOT EXISTS dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE IF NOT EXISTS dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE IF NOT EXISTS dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE IF NOT EXISTS dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE IF NOT EXISTS dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE IF NOT EXISTS dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); QueryPlan.pm000444023532023421 30627312121654175 17301 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::QueryPlan; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; # this class is an evolving attempt to formalize # the blob of cached value used for query construction class UR::DataSource::QueryPlan { is => 'UR::Value', id_by => [ rule_template => { is => 'UR::BoolExpr::Template', id_by => ['subject_class_name','logic_type','logic_detail','constant_values_id'] }, data_source => { is => 'UR::DataSource', id_by => 'data_source_id' }, ], has_transient => [ _is_initialized => { is => 'Boolean' }, needs_further_boolexpr_evaluation_after_loading => { is => 'Boolean' }, # data tracked for the whole query by property,alias,join_id _delegation_chain_data => { is => 'HASH' }, _alias_data => { is => 'HASH' }, _join_data => { is => 'HASH' }, # the old $alias_num _alias_count => { is => 'Number' }, # the old @sql_joins _db_joins => { is => 'ARRAY' }, # the new @obj_joins _obj_joins => { is => 'ARRAY' }, # the old all_table_properties, which has a small array of loading info _db_column_data => { is => 'ARRAY' }, # the old hashes by the same names _group_by_property_names => { is => 'HASH' }, _order_by_property_names => { is => 'HASH' }, _sql_filters => { is => 'ARRAY' }, _sql_params => { is => 'ARRAY' }, lob_column_names => {}, lob_column_positions => {}, query_config => {}, post_process_results_callback => {}, select_clause => {}, select_hint => {}, from_clause => {}, where_clause => {}, connect_by_clause => {}, group_by_clause => {}, order_by_columns => {}, order_by_non_column_data => {}, # flag that's true if asked to order_by something not in the data source sql_params => {}, filter_specs => {}, property_names_in_resultset_order => {}, rule_template_id => {}, rule_template_id_without_recursion_desc => {}, rule_template_without_recursion_desc => {}, joins => {}, recursion_desc => {}, recurse_property_on_this_row => {}, recurse_property_referencing_other_rows => {}, recurse_resolution_by_iteration => {}, # For data sources that don't support recursive queries joins_across_data_sources => {}, # context _resolve_query_plan_for_ds_and_bxt loading_templates => {}, class_name => {}, rule_matches_all => {}, rule_template_is_id_only => {}, sub_typing_property => {}, class_table_name => {}, rule_template_specifies_value_for_subtype => {}, sub_classification_meta_class_name => {}, ] }; sub _load { my $class = shift; my $rule = shift; # See if the requested object is loaded. my @loaded = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); return $class->context_return(@loaded) if @loaded; # Auto generate the object on the fly. my $id = $rule->value_for_id; unless (defined $id) { #$DB::single = 1; Carp::croak "No id specified for loading members of an infinite set ($class)!" } my $class_meta = $class->__meta__; my @p = (id => $id); if (my $alt_ids = $class_meta->{id_by}) { if (@$alt_ids == 1) { push @p, $alt_ids->[0] => $id; } else { my ($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule); push @p, $rule->params_list; } } my $obj = $UR::Context::current->_construct_object($class, @p); if (my $method_name = $class_meta->sub_classification_method_name) { my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule); my $sub_class_name = $obj->$method_name; if ($sub_class_name ne $class) { # delegate to the sub-class to create the object $UR::Context::current->_abandon_object($obj); $obj = $UR::Context::current->_construct_object($sub_class_name,$rule); $obj->__signal_change__("load"); return $obj; } # fall through if the class names match } $obj->__signal_change__("load"); return $obj; } # these hash keys are probably removable # because they are not above, they will be deleted if _init sets them # this exists primarily as a cleanup target list my @extra = qw( id_properties direct_table_properties all_table_properties sub_classification_method_name subclassify_by properties_meta_in_resultset_order all_properties rule_specifies_id all_id_property_names id_property_sorter properties_for_params first_table_name base_joins parent_class_objects ); sub _init { my $self = shift; Carp::confess("already initialized???") if $self->_is_initialized; # We could have this sub-classify by data source type, but right # now it's conditional logic because we'll likely remove the distinctions. # This will work because we'll separate out the ds-specific portion # and call methods on the DS to get that part. my $ds = $self->data_source; if ($ds->isa("UR::DataSource::RDBMS")) { $self->_init_light(); $self->_init_rdbms(); } elsif ($ds->isa('UR::DataSource::Filesystem')) { $self->_init_core(); $self->_init_filesystem(); } else { # Once all callers are using the API for this we won't need "_init". $self->_init_core(); $self->_init_default() if $ds->isa("UR::DataSource::Default"); #$self->_init_remote_cache() if $ds->isa("UR::DataSource::RemoteCache"); } # This object is currently still used as a hashref, but the properties # are a declaration of the part of the hashref data we are still dependent upon. # This removes the other properties to ensure this is the case. # Next steps are to clean up the code below to not produce the data, # then this loop can throw an exception if extra untracked data is found. for my $key (keys %$self) { next if $self->can($key); delete $self->{$key}; } $self->_is_initialized(1); return $self; } sub _determine_complete_order_by_list { my($self, $rule_template, $class_data, $db_property_data) = @_; my $class_meta = $rule_template->subject_class_name->__meta__; my $order_by_columns = $class_data->{order_by_columns} || []; my $order_by = $rule_template->order_by; my $ds = $self->data_source; my %order_by_property_names; my $order_by_non_column_data; if ($order_by) { my %db_property_data_map = map { $_->[1]->property_name => $_ } @$db_property_data; # we only pull back columns we're ordering by if there is ordering happening my %is_descending; my @column_data; for my $name (@$order_by) { my $order_by_prop = $name; if ($order_by_prop =~ m/^(-|\+)(.*)$/) { $order_by_prop = $2; $is_descending{$order_by_prop} = $1 eq '-'; } my($order_by_prop_meta) = $class_meta->_concrete_property_meta_for_class_and_name($order_by_prop); unless ($order_by_prop_meta) { Carp::croak("Cannot order by '$name': Class " . $class_meta->class_name . " has no property named '$order_by_prop'"); } $name = ( $is_descending{$order_by_prop} ? '-' : '' ) . $order_by_prop_meta->property_name; if ($order_by_property_names{$name} = $db_property_data_map{$order_by_prop_meta->property_name}) { # yes, single = push @column_data, $order_by_property_names{$name}; my $table_column_names = $ds->_select_clause_columns_for_table_property_data($column_data[-1]); $is_descending{$table_column_names->[0]} = $is_descending{$order_by_prop}; # copy for table.column designation $order_by_property_names{$table_column_names->[0]} = $order_by_property_names{$name}; } else { $order_by_non_column_data = 1; } } if (@column_data) { my $additional_order_by_columns = $ds->_select_clause_columns_for_table_property_data(@column_data); # Strip out columns named in the original $order_by_columns list that now appear in the # additional order by list so we don't duplicate columns names, and the additional columns # appear earlier in the list my %additional_order_by_columns = map { $_ => 1 } @$additional_order_by_columns; my @existing_order_by_columns = grep { ! $additional_order_by_columns{$_} } @$order_by_columns; $order_by_columns = [ map { $is_descending{$_} ? '-'. $_ : $_ } ( @$additional_order_by_columns, @existing_order_by_columns ) ]; } } $self->_order_by_property_names(\%order_by_property_names); return ($order_by_columns, $order_by_non_column_data); } sub _init_rdbms { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; #my $first_table_name = $class_data->{first_table_name}; #my $id_property_sorter = $class_data->{id_property_sorter}; #my @lob_column_names = @{ $class_data->{lob_column_names} }; my @lob_column_positions = @{ $class_data->{lob_column_positions} }; #my $query_config = $class_data->{query_config}; #my $post_process_results_callback = $class_data->{post_process_results_callback}; #my $class_table_name = $class_data->{class_table_name}; # individual template based my $hints = $rule_template->hints; my %hints = map { $_ => 1 } @$hints; my $order_by = $rule_template->order_by; my $group_by = $rule_template->group_by; my $limit = $rule_template->limit; my $aggregate = $rule_template->aggregate; my $recursion_desc = $rule_template->recursion_desc; my ($first_table_name, @db_joins) = _resolve_db_joins_for_inheritance($class_meta); $self->_db_joins(\@db_joins); $self->_obj_joins([]); # an array of arrays, containing $table_name, $column_name, $alias, $object_num # as joins are done we extend this, and then condense it into object fabricators my @db_property_data = @{ $class_data->{all_table_properties} }; my %group_by_property_names; if ($group_by) { # we only pull back columns we're grouping by or aggregating if there is grouping happening for my $name (@$group_by) { unless ($class_name->can($name)) { Carp::croak("Cannot group by '$name': Class $class_name has no property/method by that name"); } $group_by_property_names{$name} = 1; } for my $data (@db_property_data) { my $name = $data->[1]->property_name; if ($group_by_property_names{$name}) { $group_by_property_names{$name} = $data; } } @db_property_data = grep { ref($_) } values %group_by_property_names; } my($order_by_columns, $order_by_non_column_data) = $self->_determine_complete_order_by_list($rule_template, $class_data,\@db_property_data); $self->_db_column_data(\@db_property_data); $self->_group_by_property_names(\%group_by_property_names); # Find out what delegated properties we'll be dealing with my @sql_filters; my @delegated_properties; do { my %filters = map { $_ => 0 } grep { substr($_,0,1) ne '-' } $rule_template->_property_names; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } # Remove the flag for descending/ascending sort my @order_by_properties = $order_by ? @$order_by : ();; s/^-|\+// foreach @order_by_properties; my %properties_involved = map { $_ => 1 } keys(%filters), ($hints ? @$hints : ()), @order_by_properties, ($group_by ? @$group_by : ()); my @properties_involved = sort keys(%properties_involved); my @errors; while (my $property_name = shift @properties_involved) { if (index($property_name,'.') != -1) { push @delegated_properties, $property_name; next; } my (@pmeta) = $class_meta->property_meta_for_name($property_name); unless (@pmeta) { if ($class_name->can($property_name)) { # method, not property next; } else { push @errors, "Class ".$class_meta->id." has no property or method named '$property_name'"; next; } } # For each property in this list, go up the inheritance and find the right property # to query on. Give priority to properties that actually have columns FIND_PROPERTY_WITH_COLUMN: foreach my $pmeta ( @pmeta ) { foreach my $candidate_class ( $class_meta->all_class_metas ) { my $candidate_prop_meta = UR::Object::Property->get(class_name => $candidate_class->class_name, property_name => $property_name); next unless $candidate_prop_meta; if ($candidate_prop_meta->column_name) { $pmeta = $candidate_prop_meta; next FIND_PROPERTY_WITH_COLUMN; } } } my $property = $pmeta[0]; my $table_name = $property->class_meta->first_table_name; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); if ($property->can("expr_sql")) { unless ($table_name) { $ds->warning_message("Property '$property_name' of class '$class_name' can 'expr_sql' but has no table!"); next; } my $expr_sql = $property->expr_sql; if (exists $filters{$property_name}) { push @sql_filters, $table_name => { # cheap hack of prefixing with a whitespace differentiates # from a regular column below " " . $expr_sql => { operator => $operator, value_position => $value_position } }; } next; } # If the property is calculate and has a calculate_from list, add the # calculate_from things to the internal hints list, but not the template if ($property->is_calculated and $property->calculate_from) { my $calculate_from = $property->calculate_from; push @properties_involved, @$calculate_from; push @$hints, @$calculate_from; $hints{$_} = 1 foreach @$calculate_from; } if (my $column_name = $property->column_name) { # normal column: filter on it unless ($table_name) { $ds->warning_message("Property '$property_name' of class '$class_name' has column '$column_name' but has no table!"); next; } if (exists $filters{$property_name}) { push @sql_filters, $table_name => { $column_name => { operator => $operator, value_position => $value_position } }; } } elsif ($property->is_delegated) { push @delegated_properties, $property->property_name; } elsif ( ! exists($hints{$property_name}) or exists($filters{$property_name}) ) { $self->needs_further_boolexpr_evaluation_after_loading(1); } else { next; } } # end of properties in the expression which control the query content if (@errors) { my $class_name = $class_meta->class_name; $ds->error_message("ERRORS PROCESSING PARAMTERS: (" . join("\n", @errors) . ") used to generate SQL for $class_name!"); #print Data::Dumper::Dumper($rule_template); Carp::croak("Can't continue"); } }; my $object_num = 0; $self->_alias_count(0); my %hints_included; my @select_hint; # FIXME - this needs to be broken out into delegated-property-join-resolver # and inheritance-join-resolver methods that can be called recursively. # It would better encapsulate what's going on and avoid bugs with complicated # get()s # one iteration per target value involved in the query, # including values needed for filtering, ordering, grouping, and hints (selecting more) # these "properties" may be a single property name or an ad-hoc "chain" DELEGATED_PROPERTY: for my $delegated_property (sort @delegated_properties) { my $property_name = $delegated_property; my $delegation_chain_data = $self->_delegation_chain_data || $self->_delegation_chain_data({}); $delegation_chain_data->{"__all__"}{table_alias} = {}; $delegation_chain_data->{"__all__"}{class_alias} = { $first_table_name => $class_meta }; my ($final_accessor, $is_optional, @joins) = _resolve_object_join_data_for_property_chain($rule_template,$property_name,$property_name); # when there is no "final_accessor" it often means we have an object-accessor in a hint # we want that to go through the join process, and only be left out at filter construction time #unless ($final_accessor) { #$self->needs_further_boolexpr_evaluation_after_loading(1); #next; #} # this is gathered here and used below, but previously was gathered internally to the methods which take it # since it is no longer needed directly in this method it might be refactored into the places which use it my %ds_for_class; for my $join (@joins) { my $source_class_object = $join->{'source_class'}->__meta__; my ($source_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($source_class_object, $rule_template); $ds_for_class{$join->{'source_class'}} = $source_data_source; my $foreign_class_object = $join->{'foreign_class'}->__meta__; my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); $ds_for_class{$join->{'foreign_class'}} = $foreign_data_source; } # Splice out joins that go through a UR::Value class and back out to the DB, since UR::Value-types # don't get stored in the DB # TODO: move this into the join creation logic for (my $i = 0; $i < @joins; $i++) { if ( $i < $#joins and ( # db -> UR::Value -> db : shortcut $joins[$i]->{'foreign_class'}->isa('UR::Value') and $joins[$i+1]->{'source_class'}->isa('UR::Value') #and $joins[$i]->{'foreign_class'}->isa($joins[$i+1]->{'source_class'}) ## remove this? ) ) { my $fixed_join = UR::Object::Join->_get_or_define( source_class => $joins[$i]->{'source_class'}, source_property_names => $joins[$i]->{'source_property_names'}, foreign_class => $joins[$i+1]->{'foreign_class'}, foreign_property_names => $joins[$i+1]->{'foreign_property_names'}, is_optional => $joins[$i]->{'is_optional'}, id => $joins[$i]->{id} . "->" . $joins[$i+1]->{id}); if ($joins[$i+1]->{where}) { # If there's a where involved, it will always be on the second thing, # where the foreign_class is NOT a UR::Value $fixed_join->{where} = $joins[$i+1]->{where}; } splice(@joins, $i, 2, $fixed_join); } } if (@joins and $joins[-1]{foreign_class}->isa("UR::Value")) { # the final join in a chain is often the link between a primitive value # and the UR::Value subclass into which it falls ...irrelevent for db joins $final_accessor = $joins[-1]->source_property_names->[0]; pop @joins; next DELEGATED_PROPERTY unless @joins; } my $last_class_object_excluding_inherited_joins; my $alias_for_property_value; # one iteration per table between the start table and target while (my $object_join = shift @joins) { $object_num++; my @joins_for_object = ($object_join); # one iteration per layer of inheritance for this object # or per case of a join having additional filtering my $current_inheritance_depth_for_this_target_join = 0; while (my $join = shift @joins_for_object) { my $where = $join->{where}; $current_inheritance_depth_for_this_target_join++; my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; if ($foreign_class_object->join_hint and !($hints_included{$foreign_class_name}++)) { push @select_hint, $foreign_class_object->join_hint; } if (not exists $ds_for_class{$foreign_class_name}) { # error: we should have at least a key with an empty value if we tried to find the ds die "no data source key for $foreign_class_name when adding a join?" } my $ds = $ds_for_class{$foreign_class_name}; if (not $ds) { # no ds for the next piece of data: we will have to resolve this on the client side # this is where things may get slow if the query is insufficiently filtered $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } my $alias = $self->_add_join( $delegated_property, $join, $object_num, $is_optional, $final_accessor, $ds_for_class{$foreign_class_name}, ); if (not $alias) { # unable to add a join for another reason # TODO: is the above the only valid case of a join being impossible? # Can we remove this? $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } # set these for after all of the joins are done my $last_class_name = $foreign_class_name; my $last_class_object = $foreign_class_object; # on the first iteration, we figure out the remaining inherited iterations # if there is inheritance to do, unshift those onto the stack ahead of other things if ($current_inheritance_depth_for_this_target_join == 1) { if ($final_accessor and $last_class_object->property_meta_for_name($final_accessor)) { $last_class_object_excluding_inherited_joins = $last_class_object; } my @parents = grep { $_->table_name } $foreign_class_object->ancestry_class_metas; if (@parents) { my @last_id_property_names = $foreign_class_object->id_property_names; for my $parent (@parents) { my @parent_id_property_names = $parent->id_property_names; die if @parent_id_property_names > 1; my $parent_join_foreign_class_name = $parent->class_name; my $inheritance_join = UR::Object::Join->_get_or_define( source_class => $last_class_name, source_property_names => [@last_id_property_names], # we change content below foreign_class => $parent_join_foreign_class_name, foreign_property_names => \@parent_id_property_names, is_optional => $is_optional, id => "${last_class_name}::" . join(',',@last_id_property_names), ); unshift @joins_for_object, $inheritance_join; @last_id_property_names = @parent_id_property_names; $last_class_name = $foreign_class_name; my $foreign_class_object = $parent_join_foreign_class_name->__meta__; my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); $ds_for_class{$parent_join_foreign_class_name} = $foreign_data_source; } next; } } if (!@joins and not $alias_for_property_value) { # we are out of joins for this delegated property # setting $alias_for_property_value helps map to exactly where we do real filter/order/etc. my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object); if ($final_accessor and grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} } ) { $alias_for_property_value = $alias; #print "found alias for $property_name on $foreign_class_name: $alias\n"; } else { # The thing we're joining to isn't a database-backed column (maybe calculated?) $self->needs_further_boolexpr_evaluation_after_loading(1); next DELEGATED_PROPERTY; } } } # next join in the inheritance for this object } # next join across objects from the query subject to the delegated property target # done adding any new joins for this delegated property/property-chain # now see if anything in the where-clause needs to filter on the item joined-to my $value_position = $rule_template->value_position_for_property_name($property_name); if (defined $value_position) { # this property _is_ used to filter results if (not $final_accessor) { # on the client side :( $self->needs_further_boolexpr_evaluation_after_loading(1); next; } else { # at the database level :) my $final_accessor_property_meta = $last_class_object_excluding_inherited_joins->property_meta_for_name($final_accessor); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object_excluding_inherited_joins->class_name . " while resolving joins for property '" . $delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $self->needs_further_boolexpr_evaluation_after_loading(1); next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); unless ($alias_for_property_value) { die "No alias found for $property_name?!"; } push @sql_filters, $alias_for_property_value => { $sql_lvalue => { operator => $operator, value_position => $value_position } }; } } } # next delegated property # the columns to query my $db_property_data = $self->_db_column_data; # the following two sets of variables hold the net result of the logic my $select_clause; my $from_clause; my $connect_by_clause; my $group_by_clause; # Build the SELECT clause explicitly. $select_clause = $ds->_select_clause_for_table_property_data(@$db_property_data); # Oracle places group_by in a comment in the select unshift(@select_hint, $class_meta->select_hint) if $class_meta->select_hint; # Build the FROM clause base. # Add joins to the from clause as necessary, then $from_clause = (defined $first_table_name ? "$first_table_name" : ''); my $cnt = 0; my @sql_params; my @sql_joins = @{ $self->_db_joins }; while (@sql_joins) { my $table_name = shift (@sql_joins); my $condition = shift (@sql_joins); my ($table_alias) = ($table_name =~ /(\S+)\s*$/s); my $join_type; if ($condition->{-is_required}) { $join_type = 'INNER'; } else { $join_type = 'LEFT'; } $from_clause .= "\n$join_type join " . $table_name . " on "; # Restart the counter on each join for the from clause, # but for the where clause keep counting w/o reset. $cnt = 0; for my $column_name (keys %$condition) { next if substr($column_name,0,1) eq '-'; my $linkage_data = $condition->{$column_name}; my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}"); my ($operator, $value_position, $value, $link_table_name, $link_column_name, $left_coercion, $right_coercion) = @$linkage_data{qw/operator value_position value link_table_name link_column_name left_coercion right_coercion/}; $expr_sql = sprintf($right_coercion, $expr_sql) if ($right_coercion); $from_clause .= "\n and " if ($cnt++); if ($link_table_name and $link_column_name) { # the linkage data is a join specifier my $link_sql = "${link_table_name}.${link_column_name}"; $link_sql = sprintf($left_coercion, $link_sql) if ($left_coercion); $from_clause .= "$link_sql = $expr_sql"; } elsif (defined $value_position) { Carp::croak("Joins cannot use variable values currently!"); } else { my ($more_sql, @more_params) = $ds->_extend_sql_for_column_operator_and_value($expr_sql, $operator, $value); if ($more_sql) { $from_clause .= $more_sql; push @sql_params, @more_params; } else { # error return; } } } # next column } # next db join # build the WHERE clause by making a data structure which will be parsed outside of this module # special handling of different size lists, and NULLs, make a completely reusable SQL template very hard. my @filter_specs; while (@sql_filters) { my $table_name = shift (@sql_filters); my $condition = shift (@sql_filters); my ($table_alias) = ($table_name =~ /(\S+)\s*$/s); for my $column_name (keys %$condition) { my $linkage_data = $condition->{$column_name}; my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}"); my ($operator, $value_position, $value, $link_table_name, $link_column_name) = @$linkage_data{qw/operator value_position value link_table_name link_column_name/}; if ($link_table_name and $link_column_name) { # the linkage data is a join specifier Carp::confess("explicit column linkage in where clause?"); #$sql .= "${link_table_name}.${link_column_name} = $expr_sql"; } else { # the linkage data is a value position from the @values list unless (defined $value_position) { Carp::confess("No value position for $column_name in query!"); } push @filter_specs, [$expr_sql, $operator, $value_position]; } } # next column } # next db filter $connect_by_clause = ''; my $recurse_resolution_by_iteration = 0; if ($recursion_desc) { unless (ref($recursion_desc) eq 'ARRAY') { Carp::croak("Recursion description must be an arrayref with exactly 2 items"); } if (@$recursion_desc != 2) { Carp::croak("Recursion description must contain exactly 2 items; got ".scalar(@$recursion_desc) . ': ' . join(', ',@$recursion_desc)); } # Oracle supports "connect by" queries. if ($ds->does_support_recursive_queries eq 'connect by') { my ($this,$prior) = @{ $recursion_desc }; my $this_property_meta = $class_meta->property_meta_for_name($this); unless ($this_property_meta) { Carp::croak("Class ".$class_meta->class_name." has no property named '$this', named in the recursion description"); } my $prior_property_meta = $class_meta->property_meta_for_name($prior); unless ($prior_property_meta) { Carp::croak("Class ".$class_meta->class_name." has no property named '$prior', named in the recursion description"); } my $this_class_meta = $this_property_meta->class_meta; my $prior_class_meta = $prior_property_meta->class_meta; my $this_table_name = $this_class_meta->table_name; unless ($this_table_name) { Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$this', named in the recursion description"); } my $prior_table_name = $prior_class_meta->table_name; unless ($prior_table_name) { Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$prior', named in the recursion description"); } my $this_column_name = $this_property_meta->column_name || $this; my $prior_column_name = $prior_property_meta->column_name || $prior; $connect_by_clause = "connect by $this_table_name.$this_column_name = prior $prior_table_name.$prior_column_name\n"; } else { $recurse_resolution_by_iteration = 1; } } my @property_names_in_resultset_order; for my $property_meta_array (@$db_property_data) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } # this is only used when making a real instance object instead of a "set" my $per_object_in_resultset_loading_detail; unless ($group_by) { $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@$db_property_data, $self->_obj_joins); } if ($group_by) { # when grouping, we're making set objects instead of regular objects # this means that we re-constitute the select clause and add a group_by clause $group_by_clause = 'group by ' . $select_clause if (scalar(@$group_by)); # Q: - does it even make sense for the user to specify an order_by in the # get() request for Set objects? If so, then we need to concatonate these order_by_columns # with the ones that already exist in $order_by_columns from the class data # A: - yes, because group by means "return a list of subsets", and this lets you sort the subsets $order_by_columns = $ds->_select_clause_columns_for_table_property_data(@$db_property_data); $select_clause .= ', ' if $select_clause; $select_clause .= 'count(*) count'; for my $ag (@$aggregate) { next if $ag eq 'count'; # TODO: translate property names to column names, and skip non-column properties $select_clause .= ', ' . $ag; } unless (@$group_by == @$db_property_data) { print "mismatch table properties vs group by!\n"; } } %$self = ( %$self, # custom for RDBMS select_clause => $select_clause, select_hint => scalar(@select_hint) ? \@select_hint : undef, from_clause => $from_clause, connect_by_clause => $connect_by_clause, group_by_clause => $group_by_clause, order_by_columns => $order_by_columns, order_by_non_column_data => $order_by_non_column_data, filter_specs => \@filter_specs, sql_params => \@sql_params, recurse_resolution_by_iteration => $recurse_resolution_by_iteration, # override defaults in the regular datasource $parent_template_data property_names_in_resultset_order => \@property_names_in_resultset_order, properties_meta_in_resultset_order => $db_property_data, # duplicate?! loading_templates => $per_object_in_resultset_loading_detail, ); my $template_data = $rule_template->{loading_data_cache} = $self; return $self; } sub _init_filesystem { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @db_property_data = @{ $class_data->{all_table_properties} }; my($order_by_columns, $order_by_non_column_data) = $self->_determine_complete_order_by_list($rule_template, $class_data, \@db_property_data); %$self = ( %$self, order_by_columns => $order_by_columns, order_by_non_column_data => $order_by_non_column_data, ); my $template_data = $rule_template->{loading_data_cache} = $self; return $self; } sub _add_join { my ($self, $property_name, $join, $object_num, $is_optional, $final_accessor, $foreign_data_source, ) = @_; my $delegation_chain_data = $self->_delegation_chain_data || $self->_delegation_chain_data({}); my $table_alias = $delegation_chain_data->{"__all__"}{table_alias} ||= {}; my $source_table_and_column_names = $delegation_chain_data->{$property_name}{latest_source_table_and_column_names} ||= []; my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $class_alias = $delegation_chain_data->{"__all__"}{class_alias} ||= {}; if (! %$class_alias and $source_class_object->table_name) { $class_alias->{$source_class_object->table_name} = $source_class_object; } my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $group_by = $rule_template->group_by; #my($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (!$foreign_data_source or ($foreign_data_source ne $ds)) { # FIXME - do something smarter in the future where it can do a join-y thing in memory $self->needs_further_boolexpr_evaluation_after_loading(1); return; } my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object); # This will get filled in during the first pass, and every time after we've successfully # performed a join - ie. that the delegated property points directly to a class/property # that is a real table/column, and not a tableless class or another delegated property my @source_property_names; unless (@$source_table_and_column_names) { @source_property_names = @{ $join->{source_property_names} }; @$source_table_and_column_names = map { if ($_->[0] =~ /^(.*)\s+(\w+)\s*$/s) { # This "table_name" was actually a bit of SQL with an inline view and an alias # FIXME - this won't work if they used the optional "as" keyword $_->[0] = $1; $_->[2] = $2; } $_; } map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::croak("No property $_ for class ".$source_class_object->class_name); } my($table_name,$column_name) = $p->table_and_column_name_for_property(); if ($table_name && $column_name) { [$table_name, $column_name]; } else { #Carp::confess("Can't determine table and column for property $_ in class " . # $source_class_object->class_name); (); } } @source_property_names; } return unless @$source_table_and_column_names; #my @source_property_names = @{ $join->{source_property_names} }; #my ($source_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($source_class_object, @source_property_names); #my @source_column_names = @$fcols; #my @source_property_meta = @$fprops; my @foreign_property_names = @{ $join->{foreign_property_names} }; my ($foreign_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($foreign_class_object, @foreign_property_names); my @foreign_column_names = @$fcols; my @foreign_property_meta = @$fprops; unless (@foreign_column_names) { # all calculated properties: don't try to join any further return; } unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $self->needs_further_boolexpr_evaluation_after_loading(1); return; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $self->needs_further_boolexpr_evaluation_after_loading(1); } my $alias = $self->_get_join_alias($join); unless ($alias) { my $alias_num = $self->_alias_count($self->_alias_count+1); my $alias_name = $join->sub_group_label || $property_name; if (substr($alias_name,-1) eq '?') { chop($alias_name) if substr($alias_name,-1) eq '?'; } my $alias_length = length($alias_name)+length($alias_num)+1; my $alias_max_length = 29; if ($alias_length > $alias_max_length) { $alias = substr($alias_name,0,$alias_max_length-length($alias_num)-1); } else { $alias = $alias_name; } $alias =~ s/\./_/g; $alias .= '_' . $alias_num; $self->_set_join_alias($join, $alias); if ($foreign_class_object->table_name) { my @extra_db_filters; my @extra_obj_filters; # TODO: when "flatten" correctly feeds the "ON" clause we can remove this # This will crash if the "where" happens to use indirect things my $where = $join->{where}; if ($where) { for (my $n = 0; $n < @$where; $n += 2) { my $key =$where->[$n]; my ($name,$op) = ($key =~ /^(\S+)\s*(.*)/); #my $meta = $foreign_class_object->property_meta_for_name($name); #my $column = $meta->is_calculated ? (defined($meta->calculate_sql) ? ($meta->calculate_sql) : () ) : ($meta->column_name); my ($table_name, $column_names, $property_metas) = $self->_resolve_table_and_column_data($foreign_class_object, $name); my $column = $column_names->[0]; if (not $column) { Carp::confess("No column for $foreign_class_object->{id} $name? Indirect property flattening must be enabled to use indirect filters in where with via/to."); } my $value = $where->[$n+1]; push @extra_db_filters, $column => { value => $value, ($op ? (operator => $op) : ()) }; push @extra_obj_filters, $name => { value => $value, ($op ? (operator => $op) : ()) }; } } my @db_join_data; for (my $n = 0; $n < @foreign_column_names; $n++) { my $link_table_name = $table_alias->{$source_table_and_column_names->[$n][0]} || $source_table_and_column_names->[$n][2] || $source_table_and_column_names->[$n][0]; my $link_column_name = $source_table_and_column_names->[$n][1]; my $foreign_column_name = $foreign_column_names[$n]; my $link_class_meta = $class_alias->{$link_table_name} || $source_class_object; my $link_property_name = $link_class_meta->property_for_column($link_column_name); my @coercion = $self->data_source->cast_for_data_conversion( $link_class_meta->_concrete_property_meta_for_class_and_name($link_property_name), $foreign_property_meta[$n], ); push @db_join_data, $foreign_column_name => { link_table_name => $link_table_name, link_column_name => $link_column_name, left_coercion => $coercion[0], right_coercion => $coercion[1], }; } $self->_add_db_join( "$foreign_table_name $alias" => { @db_join_data, @extra_db_filters, } ); $self->_add_obj_join( "$alias" => { ( map { $foreign_property_names[$_] => { link_class_name => $source_class_name, link_alias => $table_alias->{$source_table_and_column_names->[$_][0]} # join alias || $source_table_and_column_names->[$_][2] # SQL inline view alias || $source_table_and_column_names->[$_][0], # table_name link_property_name => $source_property_names[$_] } } (0..$#foreign_property_names) ), @extra_obj_filters, } ); # Add all of the columns in the join table to the return list # Note that we increment the object numbers. # Note: we add grouping columns individually instead of in chunks unless ($group_by) { $self->_add_columns( map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = $object_num; $new } @{ $foreign_class_loading_data->{direct_table_properties} } ); } } if ($group_by) { if ($self->_groups_by_property($property_name)) { my ($p) = map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = 0; $new } grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} }; $self->_add_columns($p); } } if ($self->_orders_by_property($property_name)) { my ($p) = map { my $new = [@$_]; $new->[2] = $alias; $new->[3] = 0; $new } grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} }; # ??? what do we do here now with $p? } unless ($is_optional) { # if _any_ part requires this, mark it required $self->_set_alias_required($alias); } } # done adding a new join alias for a join which has not yet been done if ($foreign_class_object->table_name) { $table_alias->{$foreign_table_name} = $alias; $class_alias->{$alias} = $foreign_class_object; @$source_table_and_column_names = (); # Flag that we need to re-derive this at the top of the loop } return $alias; } sub _resolve_table_and_column_data { my ($class, $class_meta, @property_names) = @_; my @property_meta = map { $class_meta->_concrete_property_meta_for_class_and_name($_) } @property_names; my $table_name; my @column_names = map { # TODO: encapsulate if ($_->is_calculated) { if ($_->calculate_sql) { $_->calculate_sql; } else { (); } } else { my $column_name; ($table_name, $column_name) = $_->table_and_column_name_for_property(); $column_name; } } @property_meta; if ($table_name and $table_name =~ /^(.*)\s+(\w+)\s*$/s) { $table_name = $1; } return ($table_name, \@column_names, \@property_meta); } sub _set_join_alias { my ($self, $join, $alias) = @_; $self->_join_data->{$join->id}{alias} = $alias; $self->_alias_data({}) unless $self->_alias_data(); $self->_alias_data->{$alias}{join_id} = $join->id; } sub _get_join_alias { my ($self,$join) = @_; $self->_join_data({}) unless $self->_join_data(); return $self->_join_data->{$join->id}{alias}; } sub _get_alias_join { my ($self,$alias) = @_; my $alias_data = $self->_alias_data; return if (! $alias_data or ! exists($alias_data->{$alias})); my $join_id = $self->_alias_data->{$alias}{join_id}; UR::Object::Join->get($join_id); } sub _add_db_join { my ($self, $key, $data) = @_; my ($alias) = ($key =~/\w+$/); my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$alias}{db_join} = $data; my $db_joins = $self->_db_joins || $self->_db_joins([]); push @$db_joins, $key, $data; } sub _add_obj_join { my ($self, $key, $data) = @_; Carp::confess() unless ref $data; my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$key}{obj_join} = $data; # the key is the alias here my $obj_joins = $self->_obj_joins || $self->_obj_joins([]); push @$obj_joins, $key, $data; } sub _set_alias_required { my ($self, $alias) = @_; my $alias_data = $self->_alias_data || $self->_alias_data({}); $alias_data->{$alias}{is_required} = 1; $alias_data->{$alias}{db_join}{-is_required} = 1; $alias_data->{$alias}{obj_join}{-is_required} = 1; } sub _add_columns { my $self = shift; my @new = @_; my $old = $self->_db_column_data; my $pos = @$old; my $lob_column_positions = $self->{lob_column_positions}; my $lob_column_names = $self->{lob_column_names}; for my $class_property (@new) { my ($sql_class,$sql_property,$sql_table_name) = @$class_property; my $data_type = $sql_property->data_type || ''; if ($data_type =~ /LOB$/) { push @$lob_column_names, $sql_property->column_name; push @$lob_column_positions, $pos; } $pos++; } push @$old, @new; } # Used by the object fabricator to find out which resultset column a # property's data is stored sub column_index_for_class_property_and_object_num { my($self, $class_name, $property_name, $object_num) = @_; $object_num ||= 0; my $db_column_data = $self->_db_column_data; for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) { if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name and $db_column_data->[$resultset_col]->[1]->property_name eq $property_name and $db_column_data->[$resultset_col]->[3] == $object_num ) { return $resultset_col; } } return undef; } # used by the object fabricator to determine the resultset column # the source property of a join is stored. sub column_index_for_class_and_property_before_object_num { my($self, $class_name, $property_name, $object_num) = @_; return unless $object_num; my $db_column_data = $self->_db_column_data; my $index; for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) { last if ($db_column_data->[$resultset_col]->[3] >= $object_num); if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name and $db_column_data->[$resultset_col]->[1]->property_name eq $property_name ) { $index = $resultset_col; } } return $index; } sub _groups_by_property { my ($self, $property_name) = @_; return $self->_group_by_property_names->{$property_name}; } sub _orders_by_property { my ($self, $property_name) = @_; return $self->_order_by_property_names->{$property_name}; } sub _resolve_db_joins_for_inheritance { my $class_meta = $_[0]; my $first_table_name; my @sql_joins; my $prev_table_name; my $prev_id_column_name; my $prev_property_meta; my @parent_class_objects = $class_meta->ancestry_class_metas; for my $co ( $class_meta, @parent_class_objects ) { my $class_name = $co->class_name; my @id_property_objects = $co->direct_id_property_metas; my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; my $table_name = $co->table_name; if ($table_name) { $first_table_name ||= $table_name; if ($prev_table_name) { die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1; my $prev_table_alias; if ($prev_table_name =~ /.*\s+(\w+)\s*$/) { $prev_table_alias = $1; } else { $prev_table_alias = $prev_table_name; } my @coercion = $co->data_source->cast_for_data_conversion( $prev_property_meta, $id_property_objects[0]); push @sql_joins, $table_name => { $id_property_objects[0]->column_name => { link_table_name => $prev_table_alias, link_column_name => $prev_id_column_name, left_coercion => $coercion[0], right_coercion => $coercion[1], }, -is_required => 1, }; } $prev_table_name = $table_name; $prev_id_column_name = $id_property_objects[0]->column_name; $prev_property_meta = $id_property_objects[0]; } } return ($first_table_name, @sql_joins); } sub _resolve_object_join_data_for_property_chain { my ($rule_template, $property_name, $join_label) = @_; my $class_meta = $rule_template->subject_class_name->__meta__; my @joins; my $is_optional; my $final_accessor; my @pmeta = $class_meta->_concrete_property_meta_for_class_and_name($property_name); my $last_class_meta = $class_meta; for my $meta (@pmeta) { if (!$meta) { Carp::croak "Can't resolve joins for ".$rule_template->subject_class_name . " property '$property_name': No property metadata found for that class and property_name"; } #id is a special property that we want to look up, but isn't necessarily on a table #so if it aliases another property, we look at that instead if($meta->property_name eq 'id' and $meta->class_name eq 'UR::Object') { my @id_properties = grep {$_->class_name ne 'UR::Object'} $last_class_meta->id_properties; if(@id_properties == 1) { $meta = $id_properties[0]; $last_class_meta = $meta->class_name->__meta__; next; } elsif (@id_properties > 1) { Carp::confess "can't join to class " . $last_class_meta->class_name . " with multiple id properties: @id_properties"; } } if($meta->data_type and $meta->data_type =~ /::/) { $last_class_meta = UR::Object::Type->get($meta->data_type); } else { $last_class_meta = UR::Object::Type->get($meta->class_name); } last unless $last_class_meta; } # we can't actually get this from the joins because # a bunch of optional things can be chained together to form # something non-optional $is_optional = 0; for my $pmeta (@pmeta) { push @joins, $pmeta->_resolve_join_chain($join_label); $is_optional = 1 if $pmeta->is_optional or $pmeta->is_many; } return unless @joins; return ($joins[-1]->{source_name_for_foreign}, $is_optional, @joins) }; sub _init_light { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_properties = @{ $class_data->{all_properties} }; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; my $id_property_sorter = $class_data->{id_property_sorter}; my $sub_typing_property = $class_data->{sub_typing_property}; my $class_table_name = $class_data->{class_table_name}; my $recursion_desc = $rule_template->recursion_desc; my $recurse_property_on_this_row; my $recurse_property_referencing_other_rows; my $recurse_resolution_by_iteration; if ($recursion_desc) { ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc; $recurse_resolution_by_iteration = ! $ds->does_support_recursive_queries; } my $needs_further_boolexpr_evaluation_after_loading; my $is_join_across_data_source; my @sql_params; my @filter_specs; my @property_names_in_resultset_order; my $object_num = 0; # 0-based, usually zero unless there are joins my @filters = $rule_template->_property_names; my %filters = map { $_ => 0 } grep { substr($_,0,1) ne '-' } @filters; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } my ( @sql_joins, @sql_filters, $prev_table_name, $prev_id_column_name, $eav_class, @eav_properties, $eav_cnt, %pcnt, $pk_used, @delegated_properties, %outer_joins, %chain_delegates, ); for my $key (keys %filters) { if (index($key,'.') != -1) { $chain_delegates{$key} = delete $filters{$key}; } } for my $co ( $class_meta, @parent_class_objects ) { my $class_name = $co->class_name; last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) ); my @id_property_objects = $co->direct_id_property_metas; if (@id_property_objects == 0) { @id_property_objects = $co->property_meta_for_name("id"); if (@id_property_objects == 0) { Carp::confess("Couldn't determine ID properties for $class_name\n"); } } my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; for my $property_name (sort keys %filters) { my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name); next unless $property; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); delete $filters{$property_name}; $pk_used = 1 if $id_properties{ $property_name }; if ($property->is_legacy_eav) { die "Old GSC EAV can be handled with a via/to/where/is_mutable=1"; } elsif ($property->is_delegated) { push @delegated_properties, $property; } elsif ($property->is_calculated || $property->is_transient) { $needs_further_boolexpr_evaluation_after_loading = 1; } else { push @sql_filters, $class_name => { $property_name => { operator => $operator, value_position => $value_position } }; } } $prev_id_column_name = $id_property_objects[0]->column_name; } # end of inheritance loop if ( my @errors = keys(%filters) ) { my $class_name = $class_meta->class_name; $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!"); Carp::confess(); } my $last_class_name = $class_name; my $last_class_object = $class_meta; my $alias_num = 1; my %joins_done; my $joins_across_data_sources; DELEGATED_PROPERTY: for my $delegated_property (@delegated_properties) { my $last_alias_for_this_chain; my $property_name = $delegated_property->property_name; my @joins = $delegated_property->_resolve_join_chain($property_name); my $relationship_name = $delegated_property->via; unless ($relationship_name) { $relationship_name = $property_name; $needs_further_boolexpr_evaluation_after_loading = 1; } my $delegate_class_meta = $delegated_property->class_meta; my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name); next unless $via_accessor_meta; my $final_accessor = $delegated_property->to; my $data_type = $via_accessor_meta->data_type; unless ($data_type) { Carp::croak "Can't resolve delegation for $property_name on class $class_name: via property $relationship_name has no data type"; } my $data_type_meta = UR::Object::Type->get($via_accessor_meta->data_type); unless ($data_type_meta) { Carp::croak "No class meta data for " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"; } my($final_accessor_meta) = $data_type_meta->_concrete_property_meta_for_class_and_name( $final_accessor ); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } # Follow the chain of via/to delegation down to where the data ultimately lives while($final_accessor_meta->is_delegated) { # May have been 'to' an id_by/id_class_by property. Stop chaining and do two queries # If we had access to the value at this point, we could continue joining through that # value's class and id next DELEGATED_PROPERTY if ($final_accessor_meta->id_by or $final_accessor_meta->id_class_by); my $prev_accessor_meta = $final_accessor_meta; $final_accessor_meta = $final_accessor_meta->to_property_meta(); unless ($final_accessor_meta) { Carp::croak("Can't resolve property '$final_accessor' of class " . $via_accessor_meta->data_type . ": Resolution involved property '" . $prev_accessor_meta->property_name . "' of class " . $prev_accessor_meta->class_name . " which is delegated, but its via/to metadata does not resolve to a known class and property"); } } $final_accessor = $final_accessor_meta->property_name; for my $join (@joins) { my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $foreign_class_name = $join->{foreign_class}; next DELEGATED_PROPERTY if ($foreign_class_name->isa('UR::Value')); my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (! $foreign_data_source) { $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } elsif ($foreign_data_source ne $ds or ! $ds->does_support_joins or ! $foreign_data_source->does_support_joins ) { push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property); next DELEGATED_PROPERTY; } my @source_property_names = @{ $join->{source_property_names} }; my @source_table_and_column_names = map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::confess("No property $_ for class $source_class_object->{class_name}\n"); } unless ($p->class_name->__meta__) { Carp::croak("Can't get class metadata for " . $p->class_name); } [$p->class_name->__meta__->class_name, $p->property_name]; } @source_property_names; my $foreign_table_name = $foreign_class_name; unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } my @foreign_property_names = @{ $join->{foreign_property_names} }; my @foreign_property_meta = map { $foreign_class_object->_concrete_property_meta_for_class_and_name($_) } @foreign_property_names; my @foreign_column_names = map { # TODO: encapsulate $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name) } @foreign_property_meta; unless (@foreign_column_names) { # all calculated properties: don't try to join any further last; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $needs_further_boolexpr_evaluation_after_loading = 1; } my $alias = $joins_done{$join->{id}}; unless ($alias) { $alias = "${relationship_name}_${alias_num}"; $alias_num++; $object_num++; push @sql_joins, "$foreign_table_name $alias" => { map { $foreign_property_names[$_] => { link_table_name => $last_alias_for_this_chain || $source_table_and_column_names[$_][0], link_column_name => $source_table_and_column_names[$_][1] } } (0..$#foreign_property_names) }; # Add all of the columns in the join table to the return list. push @all_properties, map { [$foreign_class_object, $_, $alias, $object_num] } map { $_->[1] } # These three lines are to get around a bug in perl sort { $a->[0] cmp $b->[0] } # 5.8's sort involving method calls within the sort map { [ $_->property_name, $_ ] } # sub that do sorts of their own grep { defined($_->column_name) && $_->column_name ne '' } UR::Object::Property->get( class_name => $foreign_class_name ); $joins_done{$join->{id}} = $alias; } # Set these for after all of the joins are done $last_class_name = $foreign_class_name; $last_class_object = $foreign_class_object; $last_alias_for_this_chain = $alias; } # next join unless ($delegated_property->via) { next; } my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($final_accessor); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name . " while resolving joins for property '" .$delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $needs_further_boolexpr_evaluation_after_loading = 1; next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); } # next delegated property for my $property_meta_array (@all_properties) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } #my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@all_properties); %$self = ( %$self, %$class_data, properties_for_params => \@all_properties, property_names_in_resultset_order => \@property_names_in_resultset_order, joins => \@sql_joins, rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => $rule_template->recursion_desc, recurse_property_on_this_row => $recurse_property_on_this_row, recurse_property_referencing_other_rows => $recurse_property_referencing_other_rows, recurse_resolution_by_iteration => $recurse_resolution_by_iteration, #loading_templates => $per_object_in_resultset_loading_detail, joins_across_data_sources => $joins_across_data_sources, ); return $self; } sub _init_core { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; # TODO: most of this only applies to the RDBMS subclass, # but some applies to any datasource. It doesn't hurt to have the RDBMS stuff # here and ignored, but it's not placed correctly. # class-based values my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my @parent_class_objects = @{ $class_data->{parent_class_objects} }; my @all_properties = @{ $class_data->{all_properties} }; # my $first_table_name = $class_data->{first_table_name}; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my @all_id_property_names = @{ $class_data->{all_id_property_names} }; my @id_properties = @{ $class_data->{id_properties} }; my $id_property_sorter = $class_data->{id_property_sorter}; # my $order_by_clause = $class_data->{order_by_clause}; # my @lob_column_names = @{ $class_data->{lob_column_names} }; # my @lob_column_positions = @{ $class_data->{lob_column_positions} }; # my $query_config = $class_data->{query_config}; # my $post_process_results_callback = $class_data->{post_process_results_callback}; my $sub_typing_property = $class_data->{sub_typing_property}; my $class_table_name = $class_data->{class_table_name}; # individual query/boolexpr based my $recursion_desc = $rule_template->recursion_desc; my $recurse_property_on_this_row; my $recurse_property_referencing_other_rows; if ($recursion_desc) { ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc; } # _usually_ items freshly loaded from the DB don't need to be evaluated through the rule # because the SQL gets constructed in such a way that all the items returned would pass anyway. # But in certain cases (a delegated property trying to match a non-object value (which is a bug # in the caller's code from one point of view) or with calculated non-sql properties, then the # sql will return a superset of the items we're actually asking for, and the loader needs to # validate them through the rule my $needs_further_boolexpr_evaluation_after_loading; # Does fulfilling this request involve querying more than one data source? my $is_join_across_data_source; my @sql_params; my @filter_specs; my @property_names_in_resultset_order; my $object_num = 0; # 0-based, usually zero unless there are joins my @filters = $rule_template->_property_names; my %filters = map { $_ => 0 } grep { substr($_,0,1) ne '-' } @filters; unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") { delete $filters{'id'}; } my ( @sql_joins, @sql_filters, $prev_table_name, $prev_id_column_name, $eav_class, @eav_properties, $eav_cnt, %pcnt, $pk_used, @delegated_properties, %outer_joins, %chain_delegates, ); for my $key (keys %filters) { if (index($key,'.') != -1) { $chain_delegates{$key} = delete $filters{$key}; } } for my $co ( $class_meta, @parent_class_objects ) { # my $table_name = $co->table_name; # next unless $table_name; # $first_table_name ||= $table_name; my $class_name = $co->class_name; last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) ); my @id_property_objects = $co->direct_id_property_metas; if (@id_property_objects == 0) { @id_property_objects = $co->property_meta_for_name("id"); if (@id_property_objects == 0) { Carp::confess("Couldn't determine ID properties for $class_name\n"); } } my %id_properties = map { $_->property_name => 1 } @id_property_objects; my @id_column_names = map { $_->column_name } @id_property_objects; # if ($prev_table_name) # { # # die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1; # Carp::confess("No table for class $co->{class_name}") unless $table_name; # push @sql_joins, # $table_name => # { # $id_property_objects[0]->column_name => { # link_table_name => $prev_table_name, # link_column_name => $prev_id_column_name # } # }; # delete $filters{ $id_property_objects[0]->property_name } if $pk_used; # } for my $property_name (sort keys %filters) { my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name); next unless $property; my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); delete $filters{$property_name}; $pk_used = 1 if $id_properties{ $property_name }; # if ($property->can("expr_sql")) { # my $expr_sql = $property->expr_sql; # push @sql_filters, # $table_name => # { # # cheap hack of putting a whitespace differentiates # # from a regular column below # " " . $expr_sql => { operator => $operator, value_position => $value_position } # }; # next; # } if ($property->is_legacy_eav) { die "Old GSC EAV can be handled with a via/to/where/is_mutable=1"; } elsif ($property->is_transient) { die "Query by transient property $property_name on $class_name cannot be done!"; } elsif ($property->is_delegated) { push @delegated_properties, $property; } elsif ($property->is_calculated) { $needs_further_boolexpr_evaluation_after_loading = 1; } else { # normal column: filter on it push @sql_filters, $class_name => { $property_name => { operator => $operator, value_position => $value_position } }; } } # $prev_table_name = $table_name; $prev_id_column_name = $id_property_objects[0]->column_name; } # end of inheritance loop if ( my @errors = keys(%filters) ) { my $class_name = $class_meta->class_name; $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!"); Carp::confess(); } my $last_class_name = $class_name; my $last_class_object = $class_meta; my $alias_num = 1; my %joins_done; my $joins_across_data_sources; DELEGATED_PROPERTY: for my $delegated_property (@delegated_properties) { my $last_alias_for_this_chain; my $property_name = $delegated_property->property_name; my @joins = $delegated_property->_resolve_join_chain($property_name); #pop @joins if $joins[-1]->{foreign_class}->isa("UR::Value"); my $relationship_name = $delegated_property->via; unless ($relationship_name) { $relationship_name = $property_name; $needs_further_boolexpr_evaluation_after_loading = 1; } my $delegate_class_meta = $delegated_property->class_meta; my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name); my $final_accessor = $delegated_property->to; my($final_accessor_meta) = $via_accessor_meta->data_type->__meta__->_concrete_property_meta_for_class_and_name( $final_accessor ); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } while($final_accessor_meta->is_delegated) { $final_accessor_meta = $final_accessor_meta->to_property_meta(); unless ($final_accessor_meta) { Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type . " while resolving property $property_name on class $class_name"); } } $final_accessor = $final_accessor_meta->property_name; for my $join (@joins) { my $source_class_name = $join->{source_class}; my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__; my $foreign_class_name = $join->{foreign_class}; my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__; my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template); if (! $foreign_data_source) { $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } elsif ($foreign_data_source ne $ds or ! $ds->does_support_joins or ! $foreign_data_source->does_support_joins ) { push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property); next DELEGATED_PROPERTY; } my @source_property_names = @{ $join->{source_property_names} }; my @source_table_and_column_names = map { my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_); unless ($p) { Carp::confess("No property $_ for class $source_class_object->{class_name}\n"); } [$p->class_name->__meta__->class_name, $p->property_name]; } @source_property_names; my $foreign_table_name = $foreign_class_name; unless ($foreign_table_name) { # If we can't make the join because there is no datasource representation # for this class, we're done following the joins for this property # and will NOT try to filter on it at the datasource level $needs_further_boolexpr_evaluation_after_loading = 1; next DELEGATED_PROPERTY; } my @foreign_property_names = @{ $join->{foreign_property_names} }; my @foreign_property_meta = map { $foreign_class_object->_concrete_property_meta_for_class_and_name($_); } @foreign_property_names; my @foreign_column_names = map { # TODO: encapsulate $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name) } @foreign_property_meta; unless (@foreign_column_names) { # all calculated properties: don't try to join any further last; } unless (@foreign_column_names == @foreign_property_meta) { # some calculated properties, be sure to re-check for a match after loading the object $needs_further_boolexpr_evaluation_after_loading = 1; } my $alias = $joins_done{$join->{id}}; unless ($alias) { $alias = "${relationship_name}_${alias_num}"; $alias_num++; $object_num++; my @source_property_meta = map { $source_class_object->_concrete_property_meta_for_class_and_name($_) } @source_property_names; push @sql_joins, "$foreign_table_name $alias" => { map { my @coercion = $ds->cast_for_data_conversion( $source_property_meta[$_], $foreign_property_meta[$_]); $foreign_property_names[$_] => { link_table_name => $last_alias_for_this_chain || $source_table_and_column_names[$_][0], link_column_name => $source_table_and_column_names[$_][1], left_coercion => $coercion[0], right_coercion => $coercion[1], } } (0..$#foreign_property_names) }; # Add all of the columns in the join table to the return list. push @all_properties, map { [$foreign_class_object, $_, $alias, $object_num] } map { $_->[1] } # These three lines are to get around a bug in perl sort { $a->[0] cmp $b->[0] } # 5.8's sort involving method calls within the sort map { [ $_->property_name, $_ ] } # sub that do sorts of their own grep { defined($_->column_name) && $_->column_name ne '' } UR::Object::Property->get( class_name => $foreign_class_name ); $joins_done{$join->{id}} = $alias; } # Set these for after all of the joins are done $last_class_name = $foreign_class_name; $last_class_object = $foreign_class_object; $last_alias_for_this_chain = $alias; } # next join unless ($delegated_property->via) { next; } my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($id_properties[0]); unless ($final_accessor_property_meta) { Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name . " while resolving joins for property '" .$delegated_property->property_name . "' in class " . $delegated_property->class_name); } my $sql_lvalue; if ($final_accessor_property_meta->is_calculated) { $sql_lvalue = $final_accessor_property_meta->calculate_sql; unless (defined($sql_lvalue)) { $needs_further_boolexpr_evaluation_after_loading = 1; next; } } else { $sql_lvalue = $final_accessor_property_meta->column_name; unless (defined($sql_lvalue)) { Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name"); } } my $operator = $rule_template->operator_for($property_name); my $value_position = $rule_template->value_position_for_property_name($property_name); #push @sql_filters, # $final_table_name_with_alias => { # $sql_lvalue => { operator => $operator, value_position => $value_position } # }; } # next delegated property for my $property_meta_array (@all_properties) { push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; } my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } my @this_ds_properties = grep { ! $_->[1]->is_delegated and (! $_->[1]->is_calculated or $_->[1]->calculate_sql) } @all_properties; my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@this_ds_properties); %$self = ( %$self, %$class_data, properties_for_params => \@all_properties, property_names_in_resultset_order => \@property_names_in_resultset_order, joins => \@sql_joins, rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => $rule_template->recursion_desc, recurse_property_on_this_row => $recurse_property_on_this_row, recurse_property_referencing_other_rows => $recurse_property_referencing_other_rows, loading_templates => $per_object_in_resultset_loading_detail, joins_across_data_sources => $joins_across_data_sources, ); return $self; } sub _init_default { my $self = shift; my $bx_template = $self->rule_template; $self->{needs_further_boolexpr_evaluation_after_loading} = 1; my $all_possible_headers = $self->{loading_templates}[0]{property_names}; my $expected_headers; my $class_meta = $bx_template->subject_class_name->__meta__; for my $pname (@$all_possible_headers) { my $pmeta = $class_meta->property($pname); if ($pmeta->is_delegated) { next; } push @$expected_headers, $pname; } $self->{loading_templates}[0]{property_names} = $expected_headers; if ($bx_template->subject_class_name->isa('UR::Value')) { # Hack so the objects get blessed into the proper subclass in the Object Fabricator. # This is necessary so every possible UR::Value subclass doesn't need its # own "id" property defined. Without it, the data shows that these objects get # loaded as the base UR::Value class (since its "id" is defined on UR:Value) # and then would get automagically subclassed. $self->{'loading_templates'}->[0]->{'final_class_name'} = $bx_template->subject_class_name } return $self; } sub _init_remote_cache { my $self = shift; my $rule_template = $self->rule_template; my $ds = $self->data_source; my $class_name = $rule_template->subject_class_name; my $class_meta = $class_name->__meta__; my $class_data = $ds->_get_class_data_for_loading($class_meta); my $recursion_desc = $rule_template->recursion_desc; my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template); my $rule_template_specifies_value_for_subtype; my $sub_typing_property = $class_data->{'sub_typing_property'}; if ($sub_typing_property) { $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property) } my @property_names = $class_name->__meta__->all_property_names; %$self = ( %$self, select_clause => '', select_hint => undef, from_clause => '', where_clause => '', connect_by_clause => '', order_by_clause => '', needs_further_boolexpr_evaluation_after_loading => undef, loading_templates => [], sql_params => [], filter_specs => [], property_names_in_resultset_order => \@property_names, properties_for_params => [], rule_template_id => $rule_template->id, rule_template_without_recursion_desc => $rule_template_without_recursion_desc, rule_template_id_without_recursion_desc => $rule_template_without_recursion_desc->id, rule_matches_all => $rule_template->matches_all, rule_specifies_id => ($rule_template->specifies_value_for('id') || undef), rule_template_is_id_only => $rule_template->is_id_only, rule_template_specifies_value_for_subtype => $rule_template_specifies_value_for_subtype, recursion_desc => undef, recurse_property_on_this_row => undef, recurse_property_referencing_other_rows => undef, %$class_data, ); return $self; } 1; Code.schema000444023532023421 103312121654175 17001 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceCREATE TABLE method ( file_name varchar2(255), class_name varchar2(255), method_name varchar2(255), line_number integer, line_count integer, is_deprecated bool ); CREATE TABLE method_name_usage ( file_name varchar2(255), method_name varchar2(255), line_number integer ); CREATE INDEX method_i_method_name on method(method_name); CREATE INDEX method_name_usage_i_method_name on method_name_usage(method_name); Pg.pm000444023532023421 1107012121654175 15673 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourcepackage UR::DataSource::Pg; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::Pg', is => ['UR::DataSource::RDBMS'], is_abstract => 1, ); # RDBMS API sub driver { "Pg" } #sub server { # my $self = shift->_singleton_object(); # $self->_init_database; # return $self->_database_file_path; #} sub owner { shift->_singleton_object->login } #sub login { # undef #} # #sub auth { # undef #} sub _default_sql_like_escape_string { return '\\\\' }; sub _format_sql_like_escape_string { my $class = shift; my $escape = shift; return "E'$escape'"; } sub can_savepoint { 1;} sub set_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; $dbh->pg_savepoint($sp_name); } sub rollback_to_savepoint { my($self,$sp_name) = @_; my $dbh = $self->get_default_handle; $dbh->pg_rollback_to($sp_name); } sub _init_created_dbh { my ($self, $dbh) = @_; return unless defined $dbh; $dbh->{LongTruncOk} = 0; return $dbh; } sub _ignore_table { my $self = shift; my $table_name = shift; return 1 if $table_name =~ /^(pg_|sql_)/; } sub _get_next_value_from_sequence { my($self,$sequence_name) = @_; # we may need to change how this db handle is gotten my $dbh = $self->get_default_handle; my($new_id) = $dbh->selectrow_array("SELECT nextval('$sequence_name')"); if ($dbh->err) { die "Failed to prepare SQL to generate a column id from sequence: $sequence_name.\n" . $dbh->errstr . "\n"; return; } return $new_id; } # The default for PostgreSQL's serial datatype is to create a sequence called # tablename_columnname_seq sub _get_sequence_name_for_table_and_column { my($self,$table_name, $column_name) = @_; return sprintf("%s_%s_seq",$table_name, $column_name); } sub get_bitmap_index_details_from_data_dictionary { # FIXME Postgres has bitmap indexes, but we don't support them yet. See the Oracle # datasource module for details about how to get it working return []; } sub get_unique_index_details_from_data_dictionary { my($self,$table_name) = @_; my $sql = qq( SELECT c_index.relname, a.attname FROM pg_catalog.pg_class c_table JOIN pg_catalog.pg_index i ON i.indrelid = c_table.oid JOIN pg_catalog.pg_class c_index ON c_index.oid = i.indexrelid JOIN pg_catalog.pg_attribute a ON a.attrelid = c_index.oid WHERE c_table.relname = ? and (i.indisunique = 't' or i.indisprimary = 't') and i.indisvalid = 't' ); my $dbh = $self->get_default_handle(); return undef unless $dbh; my $sth = $dbh->prepare($sql); return undef unless $sth; #my $db_owner = $self->owner(); # We should probably do something with the owner/schema $sth->execute($table_name); my $ret; while (my $data = $sth->fetchrow_hashref()) { $ret->{$data->{'relname'}} ||= []; push @{ $ret->{ $data->{'relname'} } }, $data->{'attname'}; } return $ret; } my %ur_data_type_for_vendor_data_type = ( # DB type UR Type 'SMALLINT' => ['Integer', undef], 'BIGINT' => ['Integer', undef], 'SERIAL' => ['Integer', undef], 'TEXT' => ['XmlBlob', undef], 'BYTEA' => ['Blob', undef], 'DOUBLE PRECISION' => ['Number', undef], ); sub ur_data_type_for_data_source_data_type { my($class,$type) = @_; my $urtype = $ur_data_type_for_vendor_data_type{uc($type)}; unless (defined $urtype) { $urtype = $class->SUPER::ur_data_type_for_data_source_data_type($type); } return $urtype; } sub _alter_sth_for_selecting_blob_columns { my($self, $sth, $column_objects) = @_; for (my $n = 0; $n < @$column_objects; $n++) { next unless defined ($column_objects->[$n]); # No metaDB info for this one if (uc($column_objects->[$n]->data_type) eq 'BLOB') { require DBD::Pg; $sth->bind_param($n+1, undef, { pg_type => DBD::Pg::PG_BYTEA() }); } } } sub _value_is_null { my ($class,$value) = @_; return 1 if not defined $value; return 1 if $value eq ''; return 1 if (ref($value) eq 'HASH' and $value->{operator} eq '=' and (!defied($value->{value}) or $value->{value} eq '')); return 0; } 1; =pod =head1 NAME UR::DataSource::Pg - PostgreSQL specific subclass of UR::DataSource::RDBMS =head1 DESCRIPTION This module provides the PostgreSQL-specific methods necessary for interacting with PostgreSQL databases =head1 SEE ALSO L, L =cut RDBMS000755023532023421 012121654175 15462 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSourceFkConstraint.pm000444023532023421 1356412121654173 20611 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::FkConstraint; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::FkConstraint', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_fk_constraint', er_role => '', id_properties => [qw/data_source owner r_owner table_name r_table_name fk_constraint_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, fk_constraint_name => { type => 'varchar', len => undef, sql => 'fk_constraint_name' }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, r_owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'r_owner' }, r_table_name => { type => 'varchar', len => undef, sql => 'r_table_name' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, ], data_source => 'UR::DataSource::Meta', ); #UR::Object::Type->bootstrap_object(__PACKAGE__); sub _fk_constraint_column_class { if (shift->isa('UR::Object::Ghost')) { return 'UR::DataSource::RDBMS::FkConstraintColumn::Ghost'; } else { return 'UR::DataSource::RDBMS::FkConstraintColumn'; } } sub _table_classes { if (shift->isa('UR::Object::Ghost')) { return ('UR::DataSource::RDBMS::Table::Ghost', 'UR::DataSource::RDBMS::Table'); } else { return ('UR::DataSource::RDBMS::Table', 'UR::DataSource::RDBMS::Table::Ghost'); } } sub get_with_special_params { my($class,$rule,%args) = @_; #$DB::single = 1; my $column_name = delete $args{'column_name'}; my $r_column_name = delete $args{'r_column_name'}; my @fks = $class->get($rule); return $class->context_return(@fks) unless ($column_name || $r_column_name); my @objects; foreach my $fk ( @fks ) { my %fkc_args = ( data_source => $fk->data_source, owner => $fk->owner, table_name => $fk->table_name, r_table_name => $fk->r_table_name, ); $fkc_args{'column_name'} = $column_name if $column_name; $fkc_args{'r_column_name'} = $r_column_name if $r_column_name; my @fkc = UR::DataSource::RDBMS::FkConstraintColumn->get(%fkc_args); push @objects,$fk if @fkc; } return $class->context_return(@objects); } sub create { my $class = shift; my $params = { $class->define_boolexpr(@_)->normalize->params_list }; my $column_name = delete $params->{'column_name'}; my $r_column_name = delete $params->{'r_column_name'}; if ($column_name || $r_column_name) { $column_name = [ $column_name ] unless (ref $column_name); $r_column_name = [ $r_column_name ] unless (ref $r_column_name); unless (scalar @$column_name == scalar @$r_column_name) { Carp::confess('column_name list and r_column_name list must be the same length'); return undef; } } my $self = $class->SUPER::create($params); while ($column_name && @$column_name) { my $col_name = shift @$column_name; my $r_col_name = shift @$r_column_name; my $col_class = $self->_fk_constraint_column_class; $col_class->create(data_source => $self->data_source, owner => $self->owner, fk_constraint_name => $self->fk_constraint_name, table_name => $self->table_name, column_name => $col_name, r_table_name => $self->r_table_name, r_column_name => $r_col_name); } return $self; } sub get_related_column_objects { my($self,$prop_name) = @_; my @fkcs = UR::DataSource::RDBMS::FkConstraintColumn->get( data_source => $self->data_source, owner => $self->owner, table_name => $self->table_name, r_table_name => $self->r_table_name, fk_constraint_name => $self->fk_constraint_name, ); return @fkcs unless $prop_name; return map { $_->$prop_name } @fkcs; } sub column_names { return shift->get_related_column_objects('column_name'); } sub r_column_names { return shift->get_related_column_objects('r_column_name'); } sub column_name_map { my $self = shift; my @fkcs = $self->get_related_column_objects(); return map { [ $_->column_name, $_->r_column_name ] } @fkcs; } sub _get_related_table { my($self,$table_name) = @_; foreach my $try_class ( $self->_table_classes ) { my $table = $try_class->get(data_source => $self->data_source, table_name => $table_name); return $table if $table; } return undef; } sub get_table { my $self = shift; return $self->_get_related_table($self->table_name); } sub get_r_table { my $self = shift; return $self->_get_related_table($self->r_table_name); } 1; =pod =head1 NAME UR::DataSource::RDBMS::FkConstraint - metadata about a data source's foreign keys =head1 DESCRIPTION This class represents instances of foreign keys in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. =cut TableColumn.pm000444023532023421 726212121654174 20370 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::TableColumn; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::TableColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_table_column', er_role => '', id_properties => [qw/data_source owner table_name column_name/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, data_length => { type => 'varchar', len => undef, is_optional => 1, sql => 'data_length' }, data_type => { type => 'varchar', len => undef, sql => 'data_type' }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, nullable => { type => 'varchar', len => undef, sql => 'nullable' }, remarks => { type => 'varchar', len => undef, is_optional => 1, sql => 'remarks' }, ], data_source => 'UR::DataSource::Meta', ); # Methods moved over from the old App::DB::TableColumn sub _fk_constraint_class { my $self = shift; if (ref($self) =~ /::Ghost$/) { return "UR::DataSource::RDBMS::FkConstraint::Ghost" } else { return "UR::DataSource::RDBMS::FkConstraint" } } sub get_table { my $self = shift; my $table_name = $self->table_name; my $data_source = $self->data_source; $data_source or Carp::confess("Can't determine data_source for table $table_name column ".$self->column_name ); my $table = UR::DataSource::RDBMS::Table->get(table_name => $table_name, data_source => $data_source) || UR::DataSource::RDBMS::Table::Ghost->get(table_name => $table_name, data_source => $data_source); return $table; } sub fk_constraint_names { my @fks = shift->fk_constraints; return map { $_->fk_constraint_name } @fks; } sub fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class(); my @fks = $fk_class->get(table_name => $self->table_name, data_source => $self->data_source); return @fks; } sub bitmap_index_names { Carp::confess("not implemented yet?!"); } # the update classes code uses this. If the data type of a column is a time-ish format, then # the data_length reported by the schema is the number of bytes used internally by the DB. # Since the UR-object will store the time in text format, it will always be longer than # that. To keep $obj->__errors__ from complaining, don't even bother storing the length of # time-ish data sub is_time_data { my $self = shift; my $type = $self->data_type; if ($type =~ m/TIMESTAMP|DATE|INTERVAL/i) { return 1; } else { return; } } 1; =pod =head1 NAME UR::DataSource::RDBMS::TableColumn - metadata about a data source's table's columns =head1 DESCRIPTION This class represents instances of columns in a data source's tables. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. =cut UniqueConstraintColumn.pm000444023532023421 326712121654174 22655 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::UniqueConstraintColumn; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::UniqueConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_unique_constraint_column', id_properties => [qw/data_source owner table_name constraint_name column_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, sql => 'owner', is_optional => 1 }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, constraint_name => { type => 'varchar', len => undef, sql => 'constraint_name' }, column_name => { type => 'varchar', len => undef, sql => 'column_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::UniqueConstraintColumn - metadata about a data source's unique constraints =head1 DESCRIPTION This class represents instances of unique constraints in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. Multi-column unique constraints are represented by instances having the same table_name and constraint_name, but different column_names. =cut PkConstraintColumn.pm000444023532023421 300212121654174 21744 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::PkConstraintColumn; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::PkConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_pk_constraint_column', er_role => '', id_properties => [qw/data_source owner table_name column_name rank/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, rank => { type => 'integer', len => undef, sql => 'rank' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::PkConstraintColumn - metadata about a data source's primary keys =head1 DESCRIPTION This class represents columns that make up a primary key. Tables with multiple-column primary keys are ordered by their 'rank' property. =cut Entity.pm000444023532023421 112012121654174 17422 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::Entity; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::Entity', is => ['UR::Entity'], is_abstract => 1, data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::Meta::RDBMS::Entity - Parent class for all MetaDB-sourced classes =head1 DESCRIPTION This class exists as a means for flagging MetaDB objects and handling them specially by the infrastructure in certain circumstances, such as final data source determination. =cut Table.pm000444023532023421 1534012121654175 17227 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::Table; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::Table', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_table', id_properties => [qw/data_source owner table_name/], properties => [ data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, er_type => { type => 'varchar', len => undef, sql => 'er_type', is_optional => 1 }, last_ddl_time => { type => 'timestamp', len => undef, sql => 'last_ddl_time', is_optional => 1 }, last_object_revision => { type => 'timestamp', len => undef, sql => 'last_object_revision' }, remarks => { type => 'varchar', len => undef, is_optional => 1, sql => 'remarks' }, table_type => { type => 'varchar', len => undef, sql => 'table_type' }, ], data_source => 'UR::DataSource::Meta', ); sub _related_class_name { my($self,$subject) = @_; my $class = ref($self); # FIXME This seems kinda braindead, but is probably faster than using s/// # Is it really the right thing? my $pos = index($class, '::Table'); substr($class, $pos + 2, 5, $subject); # +2 to keep the "::" return $class; } sub _fk_constraint_class { return shift->_related_class_name('FkConstraint'); } sub _pk_constraint_class { return shift->_related_class_name('PkConstraintColumn'); } sub _unique_constraint_class { return shift->_related_class_name('UniqueConstraintColumn'); } sub _table_column_class { return shift->_related_class_name('TableColumn'); } sub _bitmap_index_class { return shift->_related_class_name('BitmapIndex'); } sub columns { my $self = shift; my $col_class = $self->_table_column_class; return $col_class->get(data_source => $self->data_source, table_name => $self->table_name); } sub column_names { return map { $_->column_name } shift->columns; } sub primary_key_constraint_columns { my $self = shift; my $pk_class = $self->_pk_constraint_class; my @pks = $pk_class->get(data_source => $self->data_source, table_name => $self->table_name); my @pks_with_rank = map { [ $_->rank, $_ ] } @pks; return map { $_->[1] } sort { $a->[0] <=> $b->[0] } @pks_with_rank; } sub primary_key_constraint_column_names { return map { $_->column_name } shift->primary_key_constraint_columns; } sub fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class; my @fks = $fk_class->get(data_source => $self->data_source, table_name => $self->table_name, owner => $self->owner); return @fks; } sub fk_constraint_names { return map { $_->fk_constraint_name } shift->fk_constraints; } sub ref_fk_constraints { my $self = shift; my $fk_class = $self->_fk_constraint_class; my @fks = $fk_class->get(data_source => $self->data_source, r_table_name => $self->table_name, r_owner => $self->owner); return @fks; } sub ref_fk_constraint_names { return map { $_->fk_constraint_name } shift->ref_fk_constraints; } sub unique_constraint_column_names { my($self,$constraint) = @_; my @c; if ($constraint) { @c = $self->unique_constraints(constraint_name => $constraint); } else { @c = $self->unique_constraints(); } my %names = map {$_->column_name => 1 } @c; return keys %names; } sub unique_constraint_names { my $self = shift; my %names = map { $_->constraint_name => 1 } $self->unique_constraints; return keys %names; } sub unique_constraints { my $self = shift; my $uc_class = $self->_unique_constraint_class; my @c = $uc_class->get( data_source => $self->data_source, table_name => $self->table_name, @_); return @c; } sub bitmap_indexes { my $self = shift; my $bi_class = $self->_bitmap_index_class; my @bi = $bi_class->get(data_source => $self->data_source, table_name => $self->table_name); return @bi; } sub bitmap_index_names { return map { $_->bitmap_index_name } shift->bitmap_indexes; } # FIXME Due to a "bug" in getting class objects, you need to pass in namespace => 'name' as # arguments to get this to work. sub handler_class { my $self = shift; return UR::Object::Type->get(table_name => $self->table_name, @_); } sub handler_class_name { my $self = shift; return $self->handler_class(@_)->class_name; } sub delete { my $self = shift; my @deleteme = ( $self->fk_constraints, $self->bitmap_indexes, $self->primary_key_constraint_columns, $self->columns, ); for my $obj ( @deleteme ) { $obj->delete; unless ($obj->isa('UR::DeletedRef')) { Carp::confess("Failed to delete $obj ".$obj->{'id'}); } } $self->SUPER::delete(); return $self; } 1; =pod =head1 NAME UR::DataSource::Meta::RDBMS::Table - Object-oriented class for RDBMS table objects. =head1 SYNOPSIS $t = UR::DataSource::Meta::RDBMS::Table->get( data_source => 'Namespace::DataSource::Name', table_name => 'MY_TABLE_NAME'); @c = $t->column_names; @f = $t->fk_constraint_names; =head1 DESCRIPTION Objects of this class represent a table in a database. They are primarily used by the class updating logic in the command line tool C, but can be retrieved and used in any application. Their instances come from from the MetaDB (L) which is partitioned and has one physical database per Namespace. =head2 Related Metadata Methods =over 4 =item @col_objs = $t->columns(); =item @col_names = $t->column_names(); =item @fk_objs = $t->fk_constraints(); =item @fk_names = $t->fk_constraint_names(); =item @ref_fk_objs = $t->ref_fk_constraints(); =item @ref_fk_names = $t->ref_fk_constraint_names(); =item @pk_objs = $t->primary_key_constraint_columns(); =item @pk_col_names = $t->primary_key_constraint_column_names(); =item @bi_objs = $t->bitmap_indexes(); =item @bi_names = $t->bitmap_index_names(); Return related metadata objects (or names) for the given table object. =back =cut BitmapIndex.pm000444023532023421 314712121654175 20366 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::BitmapIndex; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::BitmapIndex', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_bitmap_index', er_role => '', id_properties => [qw/data_source owner table_name bitmap_index_name/], properties => [ bitmap_index_name => { type => 'varchar', len => undef, sql => 'bitmap_index_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::BitmapIndex - metadata about a data source's bitmap indexes =head1 DESCRIPTION This class represents instances of bitmap indexes in a data source. They are maintained by 'ur update classes' and stored in the namespace's MetaDB. The existence of bitmap indexes in a datasource affects SQL generation during a Context commit. Oracle's implementation requires a table covered by a bitmap index to be locked while it is being updated. =cut FkConstraintColumn.pm000444023532023421 357412121654175 21751 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSuse strict; use warnings; package UR::DataSource::RDBMS::FkConstraintColumn; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::DataSource::RDBMS::FkConstraintColumn', is => ['UR::DataSource::RDBMS::Entity'], dsmap => 'dd_fk_constraint_column', er_role => 'bridge', id_properties => [qw/data_source owner table_name fk_constraint_name column_name/], properties => [ column_name => { type => 'varchar', len => undef, sql => 'column_name' }, data_source => { type => 'varchar', len => undef, sql => 'data_source' }, data_source_obj => { type => 'UR::DataSource', id_by => 'data_source'}, namespace => { calculate_from => [ 'data_source'], calculate => q( (split(/::/,$data_source))[0] ) }, fk_constraint_name => { type => 'varchar', len => undef, sql => 'fk_constraint_name' }, owner => { type => 'varchar', len => undef, is_optional => 1, sql => 'owner' }, table_name => { type => 'varchar', len => undef, sql => 'table_name' }, r_column_name => { type => 'varchar', len => undef, sql => 'r_column_name' }, r_table_name => { type => 'varchar', len => undef, sql => 'r_table_name' }, ], data_source => 'UR::DataSource::Meta', ); 1; =pod =head1 NAME UR::DataSource::RDBMS::FkConstraintColumn - metadata about a data source's foreign keys =head1 DESCRIPTION This class represents the column linkages that make up a foreign key. Each instance has a column_name (the source, where the foreign key points from) and r_column_name (remote column name, where the fireign key points to), as well as the source and remote table names. =cut TableColumn000755023532023421 012121654172 17664 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSView000755023532023421 012121654172 20576 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/TableColumnDefault000755023532023421 012121654172 22162 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/TableColumn/ViewText.pm000444023532023421 121612121654172 23601 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/TableColumn/View/Defaultpackage UR::DataSource::RDBMS::TableColumn::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['column_name', 'table_name', 'data_type', 'length', 'nullable'] }, ], ); 1; =pod =head1 NAME UR::DataSource::RDBMS::TableColumn::View::Default::Text - View class for RDBMS column objects =head1 DESCRIPTION This class defines a text-mode view for RDBMS column objects, and is used by the 'ur info' command. =cut Table000755023532023421 012121654174 16510 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMSView000755023532023421 012121654174 17422 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/TableDefault000755023532023421 012121654174 21006 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/Table/ViewText.pm000444023532023421 116112121654174 22424 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DataSource/RDBMS/Table/View/Defaultpackage UR::DataSource::RDBMS::Table::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object::View::Default::Text', has_many => [ default_aspects => { is => 'ARRAY', is_constant => 1, value => ['table_name', 'data_source', 'column_names'] }, ], ); 1; =pod =head1 NAME UR::DataSource::RDBMS::Table::View::Default::Text - View class for RDBMS table objects =head1 DESCRIPTION This class defines a text-mode view for RDBMS table objects, and is used by the 'ur info' command. =cut Service000755023532023421 012121654174 14160 5ustar00abrummetgsc000000000000UR-0.41/lib/URurinterface.js000444023532023421 1036112121654172 17201 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service// Here's the basic API: // var UR = new URInterface('http://server/api_root'); // var gsc_pse = UR.get_class('GSC::PSE'); // var pse_obj = gsc_pse.get(10001); function construct_xmlhttp() { var xmlhttp = null; if (window.XMLHttpRequest) { xmlhttp = new XMLHttpRequest(); if ( typeof xmlhttp.overrideMimeType != 'undefined') { xmlhttp.overrideMimeType('text/xml'); } } else if (window.ActiveXObject) { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } else { alert('Perhaps your browser does not support xmlhttprequests?'); } return xmlhttp; } // a URInterface holds the info to connect to the server function URInterface(base_url) { this.base_url = base_url; this.get_class = function(class_name) { return new URClassInterface(this.base_url, class_name); } this.commit = function() { var url = this.base_url + '/class/UR/Context'; do_rpc(url, 'commit', []); } } function do_rpc(url, method,arglist) { // There must be some strange scoping rules going on here. // To get the struct encoded properly, I need to copy the passed-in // array to a local one. var params = new Array; for (var i = 0; i < arglist.length; i++) { params.push(arglist[i]); } var json_rpc = { "method":method,"params":params }; //var json_rpc = { "method":method, "params":arglist }; xmlhttp = construct_xmlhttp(); xmlhttp.open('POST', url, false); post_data = json_rpc.toJSONString(); xmlhttp.send(post_data); var resultstring = xmlhttp.responseText; var resultobj = resultstring.parseJSON(); if (resultobj.error) { alert(resultobj.error); return null; } return resultobj.result; } // a URClassInterface holds the info necessary for getting instances of a class from the server function URClassInterface(base_url,class_name) { this.class_name = class_name; var path_parts = new Array; path_parts = class_name.split('::'); this.url = base_url + '/class/' + path_parts.join('/'); var result = do_rpc(this.url, '_get_class_info', []); this.id_properties = result[0]["id_properties"]; this.properties = result[0]["properties"]; this.methods = result[0]["methods"]; this.get = function() { var returned_list = do_rpc(this.url, 'get', arguments); var retval = new Array; for(var i = 0; i < returned_list.length; i++) { delete returned_list[i].db_committed; delete returned_list[i].toJSONString; var obj_url = base_url + '/obj/' + path_parts.join('/') + '/' + returned_list[i].id; var theobj = new URObject(returned_list[i], obj_url); theobj.add_methods(this.properties); theobj.add_methods(this.methods); retval.push(theobj); } return retval; }; } // Yer basic object instance from the server. For now it holds all the attributes // of an object. But we'll move it to only holding ID properties soonly function URObject(thing,url) { for (var i in thing) { this[i] = thing[i]; } this.url = url; this.tableize = function(display_location) { var table = ''; for (var i in this) { if (typeof(this[i]) == 'function') { continue; } table += ''; } table += '
' + this.object_type + '
KeyValue
' + i + '' + this[i] + '
'; var orig_data = document.getElementById(display_location).innerHTML; document.getElementById(display_location).innerHTML = orig_data + table; }; this.add_methods = function(method_names) { for (var i = 0; i < method_names.length; i++) { var method_name = method_names[i]; this[method_name] = function() { do_rpc(this.url, method_name, arguments); } } }; this.call = function(method,arglist) { //var arglist = new Array; //for (var i = 1; i < arguments.length; i++) { // arglist.push(arguments[i]); //} var returned_list = do_rpc(this.url, method, arglist); return returned_list; }; } json.js000444023532023421 2572112121654174 15653 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service/* json.js 2008-01-17 Public Domain No warranty expressed or implied. Use at your own risk. This file has been superceded by http://www.JSON.org/json2.js See http://www.JSON.org/js.html This file adds these methods to JavaScript: array.toJSONString(whitelist) boolean.toJSONString() date.toJSONString() number.toJSONString() object.toJSONString(whitelist) string.toJSONString() These methods produce a JSON text from a JavaScript value. It must not contain any cyclical references. Illegal values will be excluded. The default conversion for dates is to an ISO string. You can add a toJSONString method to any date object to get a different representation. The object and array methods can take an optional whitelist argument. A whitelist is an array of strings. If it is provided, keys in objects not found in the whitelist are excluded. string.parseJSON(filter) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional filter parameter is a function which can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. If a key contains the string 'date' then // convert the value to a date. myData = text.parseJSON(function (key, value) { return key.indexOf('date') >= 0 ? new Date(value) : value; }); It is expected that these methods will formally become part of the JavaScript Programming Language in the Fourth Edition of the ECMAScript standard in 2008. This file will break programs with improper for..in loops. See http://yuiblog.com/blog/2006/09/26/for-in-intrigue/ This is a reference implementation. You are free to copy, modify, or redistribute. Use your own copy. It is extremely unwise to load untrusted third party code into your pages. */ /*jslint evil: true */ /*members "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply, charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, parseJSON, prototype, push, replace, test, toJSONString, toString */ // Augment the basic prototypes if they have not already been augmented. if (!Object.prototype.toJSONString) { Array.prototype.toJSONString = function (w) { var a = [], // The array holding the partial texts. i, // Loop counter. l = this.length, v; // The value to be stringified. // For each value in this array... for (i = 0; i < l; i += 1) { v = this[i]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Treat objects thats lack the // toJSONString method as null. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v && typeof v.toJSONString === 'function') { a.push(v.toJSONString(w)); } else { a.push('null'); } break; case 'string': case 'number': case 'boolean': a.push(v.toJSONString()); break; default: a.push('null'); } } // Join all of the member texts together and wrap them in brackets. return '[' + a.join(',') + ']'; }; Boolean.prototype.toJSONString = function () { return String(this); }; Date.prototype.toJSONString = function () { // Eventually, this method will be based on the date.toISOString method. function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return '"' + this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z"'; }; Number.prototype.toJSONString = function () { // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(this) ? String(this) : 'null'; }; Object.prototype.toJSONString = function (w) { var a = [], // The array holding the partial texts. k, // The current key. i, // The loop counter. v; // The current value. // If a whitelist (array of keys) is provided, use it assemble the components // of the object. if (w) { for (i = 0; i < w.length; i += 1) { k = w[i]; if (typeof k === 'string') { v = this[k]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Ignore objects that lack the // toJSONString method. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v) { if (typeof v.toJSONString === 'function') { a.push(k.toJSONString() + ':' + v.toJSONString(w)); } } else { a.push(k.toJSONString() + ':null'); } break; case 'string': case 'number': case 'boolean': a.push(k.toJSONString() + ':' + v.toJSONString()); // Values without a JSON representation are ignored. } } } } else { // Iterate through all of the keys in the object, ignoring the proto chain // and keys that are not strings. for (k in this) { if (typeof k === 'string' && Object.prototype.hasOwnProperty.apply(this, [k])) { v = this[k]; switch (typeof v) { case 'object': // Serialize a JavaScript object value. Ignore objects that lack the // toJSONString method. Due to a specification error in ECMAScript, // typeof null is 'object', so watch out for that case. if (v) { if (typeof v.toJSONString === 'function') { a.push(k.toJSONString() + ':' + v.toJSONString()); } } else { a.push(k.toJSONString() + ':null'); } break; case 'string': case 'number': case 'boolean': a.push(k.toJSONString() + ':' + v.toJSONString()); // Values without a JSON representation are ignored. } } } } // Join all of the member texts together and wrap them in braces. return '{' + a.join(',') + '}'; }; (function (s) { // Augment String.prototype. We do this in an immediate anonymous function to // avoid defining global variables. // m is a table of character substitutions. var m = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; s.parseJSON = function (filter) { var j; function walk(k, v) { var i, n; if (v && typeof v === 'object') { for (i in v) { if (Object.prototype.hasOwnProperty.apply(v, [i])) { n = walk(i, v[i]); if (n !== undefined) { v[i] = n; } } } } return filter(k, v); } // Parsing happens in three stages. In the first stage, we run the text against // a regular expression which looks for non-JSON characters. We are especially // concerned with '()' and 'new' because they can cause invocation, and '=' // because it can cause mutation. But just to be safe, we will reject all // unexpected characters. // We split the first stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace all backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/.test(this.replace(/\\./g, '@'). replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the second stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + this + ')'); // In the optional third stage, we recursively walk the new structure, passing // each name/value pair to a filter function for possible transformation. return typeof filter === 'function' ? walk('', j) : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('parseJSON'); }; s.toJSONString = function () { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can simply slap some quotes around it. // Otherwise we must also replace the offending characters with safe // sequences. if (/["\\\x00-\x1f]/.test(this)) { return '"' + this.replace(/[\x00-\x1f\\"]/g, function (a) { var c = m[a]; if (c) { return c; } c = a.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }) + '"'; } return '"' + this + '"'; }; })(String.prototype); }JsonRpcServer.pm000444023532023421 2634512121654174 17452 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Servicepackage UR::Service::JsonRpcServer; use strict; use warnings; use UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => 'UR::Object', properties => [ host => { type => 'String', is_transient => 1, default_value => '0.0.0.0', doc => 'The local address to listen on'}, port => { type => 'String', is_transient => 1, default_value => 8080, doc => 'The local port to listen on'}, server => { type => 'Net::HTTPServer', is_transient => 1, doc => 'The Net::HTTPServer instance for this Server instance' }, api_root => { type => 'String', is_transient => 1, default_value => 'URapi' }, ], id_by => ['host','port'], doc => 'An object serving as a web server to respond to JSON-RPC requests; wraps Net::HTTPServer', ); =pod =head1 NAME UR::Service::JsonRpcServer - A self-contained JSON-RPC server for UR namespaces =head1 SYNOPSIS use lib '/path/to/your/moduletree'; use YourNamespace; my $rpc = UR::Service::JsonRpcServer->create(host => 'localhost', port => '8080', api_root => 'URapi', docroot => '/html/pages/path', ); $rpc->process(); =head1 Description This is a class containing an implementation of a JSON-RPC server to respond to requests involving UR-based namespaces and their objects. It uses Net::HTTPServer as the web server back-end library. Incoming requests are divided into two major categories: =over 4 =item http://server:port/C/class/Namespace/Class This is the URL for a call to a class metnod on C =item http://server:port/C/obj/Namespace/Class/id This is the URL for a method call on an object of class Namespace::Class with the given id =back =head1 Constructor The constructor takes the following named parameters: =over 4 =item host The hostname to listen on. This can be an ip address, host name, or undef. The default value is '0.0.0.0'. This argument is passed along verbatim to the Net::HTTPServer constructor. =item port The TCP port to listen on. The default value is 8080. This argument is passed along verbatim to the Net::HTTPServer constructor. =item api_root The root path that the http server will listen for requests on. The constructor registers two paths with the Net::HTTPServer with RegisterRegex() for /C/class/* and /C/obj/* to respond to class and instance metod calls. =back All other arguments are passed along to the Net::HTTPServer constructor. =head1 Methods =over 4 =item $rpc->process() A wrapper to the Net::HTTPServer Process() method. With no arguments, this call will block forever from the perspective of the caller, and process all http requests coming in. You can optionally pass in a timeout value in seconds, and it will respond to requests for the given number of seconds before returning. =back =head1 Client Side There are (or will be) client-side code in both Perl and Javascript. The Perl code is (will be) implemented as a UR::Context layer that will return light-weight object instances containing only class info and IDs. All method calls will be serialized and sent over the wire for the server process to execute them. The Javascript interface is defined in the file urinterface.js. An example: var UR = new URInterface('http://localhost:8080/URApi/'); // Connect to the server var FooThingy = UR.get_class('Foo::Thingy'); // Get the class object for Foo::Thingy var thingy = FooThingy.get(1234); // Retrieve an instance with ID 1234 var result = thingy.call('method_name', 1, 2, 3); // Call $thingy->method_name(1,2,3) on the server =head1 SEE ALSO Ney::HTTPServer, urinterface.js =cut use Net::HTTPServer; use JSON; use Class::Inspector; sub create { my($class,%args) = @_; my $api_root = delete $args{'api_root'}; my $server = Net::HTTPServer->new(%args); return unless $server; my %create_args = ( host => $args{'host'}, port => $args{'port'} ); $create_args{'api_root'} = $api_root if defined $api_root; my $self = $class->SUPER::create(%create_args); return unless $self; $self->server($server); $server->RegisterRegex("^/$api_root/class/*", sub { $self->_api_entry_classes(@_) } ) if $api_root; $server->RegisterRegex("^/$api_root/obj/*", sub { $self->_api_entry_obj(@_) } ) if $api_root; my $port = $server->Start(); if ($args{'port'} eq 'scan') { $self->port($port); } return $self; } sub process { my $self = shift; #$self->server->Process(@_); my $server = $self->server; $server->Process(@_); } sub _api_entry_classes { my($self,$request) = @_; my $response = $request->Response(); #$DB::single = 1; my $data = $self->_get_post_data_from_request($request); #my $struct = decode_json($data); my $struct = jsonToObj($data); my $class = $self->_parse_class_from_request($request); unless ($class) { $response->Code(404); $response->Print("Couldn't parse URL " . $request->URL); return $response; } my $method = $struct->{'method'}; my $params = $struct->{'params'}; my @retval; if ($method eq '_get_class_info') { # called when the other end gets a class object eval { my $class_object = $class->__meta__; my %id_names = map { $_ => 1 } $class_object->all_id_property_names(); my @id_names = keys(%id_names); my %property_names = map { $_ => 1 } grep { ! exists $id_names{$_} } $class_object->all_property_names(); my @property_names = keys(%property_names); my $possible_method_names = Class::Inspector->methods($class, 'public'); my @method_names = grep { ! exists $id_names{$_} and ! exists $property_names{$_} } @$possible_method_names; push @retval, { id_properties => \@id_names, properties => \@property_names, methods => \@method_names }; }; } else { eval { @retval = $class->$method(@$params); }; } my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}, result => \@retval}; if ($@) { $return_struct->{'result'} = undef; $return_struct->{'error'} = $@; } else { foreach my $item ( @retval ) { my $reftype = ref $item; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { # If it's an object of some sort my %copy = %$item; $copy{'object_type'} = $class; $item = \%copy; } } $return_struct->{'result'} = \@retval; } #my $encoded_result = to_json($return_struct, {convert_blessed => 1}); my $encoded_result = objToJson($return_struct); $response->Print($encoded_result); return $response; } sub _api_entry_obj { my($self,$request) = @_; my $response = $request->Response(); #$DB::single = 1; my $data = $self->_get_post_data_from_request($request); #my $struct = decode_json($data); my $struct = jsonToObj($data); my($class,$id) = $self->_parse_class_and_id_from_request($request); unless ($class) { $response->Code(404); $response->Print("Couldn't parse URL " . $request->URL); return $response; } my $method = $struct->{'method'}; my $params = $struct->{'params'}; my @retval; eval { my $obj = $class->get($id); @retval = $obj->$method(@$params); }; my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}}; if ($@) { $return_struct->{'result'} = undef; $return_struct->{'error'} = $@; } else { foreach my $item ( @retval ) { my $reftype = ref $item; if ($reftype && $reftype ne 'ARRAY' && $reftype ne 'HASH') { # If it's an object of some sort my %copy = %$item; $copy{'object_type'} = $class; $item = \%copy; } } $return_struct->{'result'} = \@retval; } #my $encoded_result = to_json($return_struct, {convert_blessed => 1}); my $encoded_result = objToJson($return_struct); $response->Print($encoded_result); return $response; } ## This one uses the last part of the URL as the ID - won't work with a generic get() #sub old_api_entry_point { # my($self,$request) = @_; # # my $response = $request->Response(); # ##$DB::single = 1; # my $data = $self->_get_post_data_from_request($request); # my $struct = decode_json($data); # # my($class,$id) = $self->_parse_class_and_id_from_request($request); # unless ($class) { # $response->Code(404); # $response->Print("Couldn't parse URL " . $request->URL); # return $response; # } # # my $method = $struct->{'method'}; # my $params = $struct->{'params'}; # my @retval; # eval { # my $obj = $class->get($id); # if ($method eq 'get') { # my %copy = %$obj; # $retval[0] = \%copy; # } else { # @retval = $obj->$method(@$params); # } # }; # # my $return_struct = { id => $struct->{'id'}, version => $struct->{'version'}}; # if ($@) { # $return_struct->{'result'} = undef; # $return_struct->{'error'} = $@; # } else { # $return_struct->{'result'} = \@retval; # } # # # my $encoded_result = to_json($return_struct, {convert_blessed => 1}); # $response->Print($encoded_result); # # return $response; #} # URLs are expected to look something like this: # http://server/URapi/Namespace/Class/Name/ID # and would translate to the class Namespace::Class::Name with the ID property ID sub _parse_class_and_id_from_request { my($self,$request) = @_; my $api_root = $self->api_root; my $url = $request->URL(); my @api_root = split(/\//,$api_root); my @url_parts = split(/\//, $url); shift @url_parts until ($url_parts[0]); { no warnings 'uninitialized'; while($api_root[0] eq $url_parts[0]) { shift @api_root; shift @url_parts; } } shift @url_parts if ($url_parts[0] eq 'class' || $url_parts[0] eq 'obj'); my $id = pop @url_parts; my $class = join('::', @url_parts); return($class,$id); } # This works for URLs that don't have an ID at the end sub _parse_class_from_request { my($self,$request) = @_; my $api_root = $self->api_root; my $url = $request->URL(); my @api_root = split(/\//,$api_root); my @url_parts = split(/\//, $url); shift @url_parts until ($url_parts[0]); { no warnings 'uninitialized'; while($api_root[0] eq $url_parts[0]) { shift @api_root; shift @url_parts; } } shift @url_parts if ($url_parts[0] eq 'class' || $url_parts[0] eq 'obj'); my $class = join('::', @url_parts); return $class; } sub _get_post_data_from_request { my($self,$request) = @_; my $message = $request->Request; my($data) = ($message =~ m/\r\n\r\n(.*)/m); return $data; } 1; RPC000755023532023421 012121654174 14604 5ustar00abrummetgsc000000000000UR-0.41/lib/UR/ServiceServer.pm000444023532023421 602412121654173 16546 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service/RPCpackage UR::Service::RPC::Server; use UR; use IO::Select; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; # We're going to be essentially reimplementing an Event queue here. :( class UR::Service::RPC::Server { has => [ 'select' => { is => 'IO::Select' }, timeout => { is => 'Float', default_value => undef }, executers => { is => 'HASH', doc => 'maps file handles to the UR::Service::RPC::Executer objects we are working with' }, ], }; sub create { my($class, %args) = @_; unless ($args{'executers'}) { $args{'executers'} = {}; } unless ($args{'select'}) { my @fh = map { $_->fh } values %{$args{'executers'}}; $args{'select'} = IO::Select->new(@fh); } my $self = $class->SUPER::create(%args); return $self; } sub add_executer { my($self,$executer,$fh) = @_; unless ($fh) { if ($executer->can('fh')) { $fh = $executer->fh; } else { $self->error_message("Cannot determine file handle for RPC executer $executer"); return; } } $self->{'executers'}->{$fh} = $executer; $self->select->add($fh); } sub loop { my $self = shift; my $timeout; if (@_) { $timeout = shift; } else { $timeout = $self->timeout; } my @ready = $self->select->can_read($timeout); my $count = 0; foreach my $fh ( @ready ) { my $executer = $self->{'executers'}->{$fh}; unless ($executer) { $self->error_message("Cannot determine RPC executer for file handle $fh fileno ",$fh->fileno); return; } $count++; unless ($executer->execute($self) ) { # they told us they were done $self->select->remove($fh); delete $self->{'executers'}->{$fh}; } } return $count; } 1; =pod =head1 NAME UR::Service::RPC::Server - Class for implementing RPC servers =head1 SYNOPSIS my $executer = Some::Exec::Class->create(fh => $fh); my $server = UR::Service::RPC::Server->create(); $server->add_executer($executer); $server->loop(5); # Process messages for 5 seconds =head1 DESCRIPTION The RPC server implementation isn't fleshed out very well yet, and may change in the future. =head1 METHODS =over 4 =item add_executer $server->add_executer($exec); Incorporate a new UR::Service::RPC::Executer instance to this server. It adds the Executer's filehandle to its own internal IO::Select object. =item loop $server->loop(); $server->loop(0); $server->loop($timeout); Enter the Server's event loop for the given number of seconds. If the timeout is undef, it will stay in the loop forever. If the timeout is 0, it will make a single pass though the readable filehandles and call C on their Executer objects. If the return value of an Executer's C method is false, that Executer's file handle is removed from the internal Select object. =back =head1 SEE ALSO UR::Service::RPC::Executer, UR::Service::RPC::Message =cut Executer.pm000444023532023421 1252412121654173 17106 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service/RPCpackage UR::Service::RPC::Executer; use UR; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; class UR::Service::RPC::Executer { has => [ fh => { is => 'IO::Handle', doc => 'handle we will send and receive messages across' }, ], has_optional => [ use_sigio => { is => 'Boolean', default_value => 0 }, ], is_transactional => 0, }; sub create { my $class = shift; my $obj = $class->SUPER::create(@_); return unless $obj; if ($obj->use_sigio) { UR::Service::RPC->enable_sigio_processing($obj); } $obj->create_subscription(method => 'use_sigio', callback => sub { my ($changed_object, $changed_property, $old_value, $new_value) = @_; return 1 if ($old_value == $new_value); if ($new_value) { UR::Service::RPC->enable_sigio_processing($obj); } else { UR::Service::RPC->disable_sigio_processing($obj); } }); return $obj; } # sub classes can override this # If they're going to reject the request, $msg should be modified in place # with a return value and exception, because we'lre going to return it right back # to the requester sub authenticate { # my($self,$msg) = @_; return 1; } # Process one message off of the file handle sub execute { my $self = shift; my $msg = UR::Service::RPC::Message->recv($self->fh); unless ($msg) { # The other end probably closed the socket $self->close_connection(); return 1; } my $response; if ($self->authenticate($msg)) { my $target_class = $msg->target_class || ref($self); my $method = $msg->method_name; my @arglist = $msg->param_list; my $wantarray = $msg->wantarray; my %resp_msg_args = ( target_class => $target_class, method_name => $method, params => \@arglist, 'wantarray' => $wantarray, fh => $self->fh ); my $method_name = join('::',$target_class, $method); if ($wantarray) { my @retval; eval { no strict 'refs'; @retval = &{$method_name}(@arglist); }; $resp_msg_args{'return_values'} = \@retval unless ($@); } elsif (defined $wantarray) { my $retval; eval { no strict 'refs'; no warnings; $retval = &{$method_name}(@arglist); }; $resp_msg_args{'return_values'} = [$retval] unless ($@); } else { eval { no strict 'refs'; &{$method_name}(@arglist); }; } $resp_msg_args{'exception'} = $@ if $@; $response = UR::Service::RPC::Message->create(%resp_msg_args); } else { # didn't authenticate. $response = $msg; } unless ($response->send()) { $self->fh->close(); } return 1; } sub close_connection { my $self = shift; $self->use_sigio(0); $self->fh->close(); } 1; =pod =head1 NAME UR::Service::RPC::Executer - Base class for modules implementing RPC executers =head1 DESCRIPTION This class is an abstract base class used to implement RPC executers. That is, modules meant to have their methods called from another process, and have the results passed back to the original caller. The communication happens over a read-write filehandle such as a socket by passing L objects back and forth. Executors are subordinate to a L object which handles decoding the message passed over the socket, calling the method on the correct executor in the right context, and returning the result back through the file handle. =head1 PROPERTIES =over 4 =item fh => IO::Handle File handle messages are received on and responses are sent to =item use_sigio => Boolean If true, the Server will set up a callback on the IO signal to handle execution, so the Server does not need to block in loop(). =back =head1 METHODS =over 4 =item authenticate $bool = $exec->authenticate($msg); This is called by execute() after the message object is deserialized from the filehandle. The default implementation just returns true. Subclasses can override this to examine the UR::Service::RPC::Message object and return true or fale whether it should allow or disallow execution. If authentication fails, the Executor should modify the Message object in-place with a proper return value and exception. =item execute $exec->execute(); Called when the Server detects data is available to read on its file handle. It deserializes the message and calls authenticate. If authentication fails, it immediately passes the message object back to the caller. If authentication succeedes, it calls the appropriate method in the Executor package, and creates a new Message object with the return value to pass back to the caller. =item close_connection $exec->close_connection(); Called by execute() when it detects that the file handle has closed. =back Derived classes should define additional methods that then become callable by execute(). =head1 SEE ALSO UR::Service::RPC::Server, UR::Service::RPC::Message =cut Message.pm000444023532023421 1266712121654174 16717 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service/RPCpackage UR::Service::RPC::Message; use UR; use FreezeThaw; use IO::Select; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Service::RPC::Message', has => [ target_class => { is => 'String' }, method_name => { is => 'String' }, ], has_optional => [ #arg_list => { is => 'ARRAY' }, params => { is => 'Object', is_many => 1 }, return_values => { is => 'Object', is_many => 1 }, 'wantarray' => { is => 'Integer' }, fh => { is => 'IO::Handle' }, exception => { is => 'String' }, ], is_transactional => 0, ); sub create { my($class,%params) = @_; foreach my $key ( 'params', 'return_values' ) { if (!$params{$key}) { $params{$key} = []; } elsif (ref($params{$key}) ne 'ARRAY') { $params{$key} = [ $params{$key} ]; } } return $class->SUPER::create(%params); } sub send { my $self = shift; my $fh = shift; $fh ||= $self->fh; my %struct; foreach my $key ( qw (target_class method_name params wantarray return_values exception) ) { $struct{$key} = $self->{$key}; } my $string = FreezeThaw::freeze(\%struct); $string = pack('N', length($string)) . $string; my $len = length($string); my $sent = 0; while($sent < $len) { my $wrote = $fh->syswrite($string, $len - $sent, $sent); if ($wrote) { $sent += $wrote; } else { # The filehandle closed for some reason $fh->close; return undef; } } return $sent; } sub recv { my($class, $fh, $timeout) = @_; # You can also call recv on a message object previously created if (ref($class) && $class->isa('UR::Service::RPC::Message')) { my $fh = $class->fh; $class = ref($class); return $class->recv($fh); } if (@_ < 3) { # # if they didn't specify a timeout $timeout = 5; # Default wait 5 sec } my $select = IO::Select->new($fh); # read in the message len, 4 chars my $msglen; my $numchars = 0; while ($numchars < 4) { unless ($select->can_read($timeout)) { $class->warning_message("Can't get message length, timed out"); return; } my $read = $fh->sysread($msglen, 4-$numchars, $numchars); unless ($read) { $class->warning_message("Can't get message length: $!"); return; } $numchars += $read; } $msglen = unpack('N', $msglen); my $string = ''; $numchars = 0; while ($numchars < $msglen) { unless ($select->can_read($timeout)) { $class->warning_message("Timed out reading message after $numchars bytes"); return; } my $read = $fh->sysread($string, $msglen - $numchars, $numchars); unless($read) { $class->warning_message("Error reading message after $numchars bytes: $!"); return; } $numchars += $read; } my($struct) = FreezeThaw::thaw($string); my $obj = $class->create(%$struct, fh => $fh); return $obj; } 1; =pod =head1 NAME UR::Service::RPC::Message - Serializable object appropriate for sending RPC messages =head1 SYNOPSIS my $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); $msg->send($fh); my $resp = UR::Service::RPC::Message->recv($fh, 5); =head1 DESCRIPTION This class is used as a message-passing interface by the RPC service modules. =head1 PROPERTIES These properties should be filled in by the initiating caller =over 4 =item method_name => Text The name of the subroutine the initiator whishes to call. =item target_class => Text The namespace the initiator wants the subroutine to be called in =item params => ARRAY List of parameters to pass to the subroutine =item wantarray => Boolean What wantarray() context the subroutine should be called in. =back These properties are assigned after the RPC call to the subroutine =over 4 =item return_values => ARRAY List of values returned by the subroutine =item exception On the receiving side, the subroutine is called within an eval. If there was an exception, C stores the value of $@, or the empty string. The receiving side should also fill-in C if there was an authentication failure. =item fh C fills this in with the file handle the message was read from. =back =head1 METHODS =over 4 =item send $bytes = $msg->send($fh); Serializes the Message object with FreezeThaw and writes the data to the filehandle $fh. Returns the number of bytes written. $bytes will be false if there was an error. =item recv $response = UR::Service::RPC::Message->recv($fh,$timeout); $response = $msg->recv(); Reads a serialized Message from the filehandle and constructs a Message object that is then returned to the caller. In the first case, it reads from the given filehandle, waiting a maximum of $timeout seconds with select before giving up. In the second case, it reads from whatever filehandle is stored in $msg to read data from. =back =head1 SEE ALSO UR::Service::RPC::Server, UR::Service::RPC::Executor =cut TcpConnectionListener.pm000444023532023421 163412121654174 21557 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Service/RPCpackage UR::Service::RPC::TcpConnectionListener; use UR; use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; class UR::Service::RPC::TcpConnectionListener { is => 'UR::Service::RPC::Executer', }; sub execute { my($self,$rpcserver) = @_; my $fh = $self->fh; my $socket = $fh->accept(); unless ($self->authenticate($socket)) { $socket->close(); return; } my $exec = $self->create_worker($socket); $rpcserver->add_executer($exec); return $exec; } # Sub classes can override this sub authenticate { # my($self,$new_socket) = @_; return 1; } # Child classes can override either of these to get custom behavior sub worker_class_name { 'UR::Service::RPC::Executer'; } sub create_worker { my($self,$new_socket) = @_; my $class = $self->worker_class_name; my $exec = $class->create(fh => $new_socket); return $exec; } 1; Manual000755023532023421 012121654175 13776 5ustar00abrummetgsc000000000000UR-0.41/lib/URSchemaDesign.pod000444023532023421 237412121654172 17174 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::SchemaDesign - Tips for designing an efficient schema for UR =head1 Relational Databases =over 4 =item Avoid creating a table called 'type' or 'types'. When 'ur update classes' translates it into a class name, it will become YourNamespace::Type. Class names ending in '::Type' are reserved for class metadata, the class will be renamed to 'YourNamespace::TypeTable' to avoid the conflict. The table_name for that class will still refer to the actual table name. 'ur update classes' will print a warning if this happens, and rename the class automatically. =item Avoid columns named 'id' UR expects an object to be uniquely identified by a property called 'id'. Classes cannot have multiple ID properties where one of them is called 'id', because 'id' would no uniqiely identify one of them. If you want to call the column 'id', then the property name in the class metadata must be something else ('id_id', for example) in both the 'has' and 'id_by' sections, and the column_name set to 'id'. =item Indexes for common queries Create indexes in your database to cover common queries. If you routinely make queries involving non-primary keys, creating an index that includes these other columns will improve query times. =back =pod Metadata.pod000444023532023421 706612121654173 16366 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::Metadata - Overview of the metadata classes in UR =head1 SYNOPSIS use MyNamespace; my $class_meta = MyNamespace::SomeClass->__meta__; my @property_metas = $class_meta->direct_property_metas(); my @parent_class_metas = $class_meta->ancestry_class_metas(); my $table_obj = UR::DataSource::RDBMS::Table->get( table_name => $class_meta->table_name, ); my @column_objs = $table_obj->columns(); =head1 DESCRIPTION The UR system creates and uses several classes and objects to represent information about the many classes and objects in the system - metadata. For example, for each class, there is an object, called a class metadata object, to represent it. Each property in a class has metadata. So does the relationship between parent and child classes and relationships involved in delegated properties. metadata about any database schemas your namespace knows about is also tracked and stored. These classes define an API for introspection and reflection, a way for the system to change itself as it runs, and methods for tracking changes and applying those changes to files and databases. =head1 APIs The metadata API is divided into 5 primary parts: =over 4 =item Defining Classes The mechanism for defining class structure, including their properties and relationships. It handles creating accessor/mutator methods for you. The syntax for defining classes is detailed in the L page. =item Objects Representing Classes, Properties, Relationships, etc. UR Classes aren't just conceptual entities like a package name, they have object instances to represent them. For every named class, you can get a L instance with that C. Each property defined on that class has a L with a matching C and C pair. Even those basic metadata classes have class, property and relationship metadata of their own. =item Schema Objects If you use the C command-line tool to manage the linkage between your database schema(s) and class structure (it's not necessary; you can also manage it by hand), then objects will also exist to represent the database entities. See also L =over 2 =item . tables L =item . columns L =item . Foreign key constraints L and L =item . Primary key constraints L =item . Unique constraints L =back =item Namespaces, Contexts and Data Sources Namespaces (L) collect and manage groups of related classes. Classes can be a member of one Namespace, and in practice will live in a subdirectory under the Namespace module's name. Contexts (L) and Data Sources (L) provide a filtered view of the data that is reachable through the current Namespace. =item Index, Change, Observer and other incidentals And then there's everything else L objects are created by the system to handle get() requests for non-ID parameters. L objects represent a change in the system during a software transaction, such as an object's property changind value or creating a new instance of something. L objects manage the change subscription system, where the application can be notified of changes through callbacks. See also L. =back UR_Presentation.pdf000444023532023421 237217712121654173 20010 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual%PDF-1.3 % 4 0 obj << /Length 5 0 R /Filter /FlateDecode >> stream x1O07&C\ A*WCLD{ r}(Й@klnlqtp =xȴEPԘc{E18LHKbДW5º޶3. /~HO)#,a++mU>L)$K=YfG$ζ/˛B\Rz2+LI2|'̘V ]_6nyV׶tVnk( p{7*w endstream endobj 5 0 obj 269 endobj 2 0 obj << /Type /Page /Parent 3 0 R /Resources 6 0 R /Contents 4 0 R /MediaBox [0 0 612 792] >> endobj 6 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> >> endobj 9 0 obj << /Length 10 0 R /N 3 /Alternate /DeviceRGB /Filter /FlateDecode >> stream xMHaї$T& R+SeL b}wg-E"u.VDNC:DuE^";cT03y|URcE4`λޘvztLUF\)s:k-iYj6|vP4*wd>,y vڴ=S԰79 ڸ@`ӋmvUl5`P=Gj)kP*}6~^/~.~a2 nײ0%f|U 9l7?j`l7"tiNf]?uhgM Zʲ4i[&LY_x {xO$̥߬S]%֧&7g̞>r=g8`候 8rʶ<dWT'<eL~.u"A=9뗚]>313X3-$e}u,gmg664$ыEzL*LZ_j_]Xy[?Xs N/ ]|msϚƫk_WfȸA2)oz-di2|m٣j|5ԥej8ɮeE7[Q|IM%ײxf)|6\ k`Ҳ䍐.> stream xUQo0~WܞH !d{[Jiݘ!Lm*;pٚM)8>= <2(Pť"l֮ؾw3ϼ 0'A19.HJb ~v 4Qnph?;l 2~A>&6IBǵ < ԆCrbpE$!2}x<$:Id,K^-+FB7+0 Ja~8=p:yya`o k^IU҂k KOM.+=^!`$%hæeؠo*tIJZ.uKw˵VBZS|*:2:WV^czu^eQ ie67XR'hjz43NBBKІ/ > endobj 14 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F3.1 17 0 R /F2.0 15 0 R /F1.0 8 0 R >> >> endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode >> stream x=;0 @IV^L E0 *@`پ;}-h}Bɪ'~|q&eҗMx#O#`Ay>TYd"btdK`vA\afݭr4yV~۹+ #§,8J)LrPb>)kt7YAB0 ɮ!_!s=Q endstream endobj 20 0 obj 192 endobj 18 0 obj << /Type /Page /Parent 3 0 R /Resources 21 0 R /Contents 19 0 R /MediaBox [0 0 612 792] >> endobj 21 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> /XObject << /Im1 22 0 R >> >> endobj 22 0 obj << /Length 23 0 R /Type /XObject /Subtype /Image /Width 476 /Height 893 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xه߹{/R-QTDEbAD"؍1hFcIl1h_4M4E{C3;}濿3<33gGHH >^z}/%~ ~}/n[F$@$8m-u6nuM\EY! 47kr3t6HD`Ony1v,\[M˪ @F Gs ;KEf @,^>oѷpe?ԚU  xw-rRڑ H~|鷮ɢuYi9y"c{,H ;`{+pG1fف– @z+wQnُ,'cH78"]I2A`͚W.Ѳ-@9y)g`#H"$kť`cKɵ/ɢHRN/wߋ\Hٮ+NSNQ OϾ瞥_d8yJ);Y @j |jSW?x1c'{}যYO)7v2.ܩ̛H _a7OOlw2/>WRxа$@!Xd7}zJp2vc% @pdZ'3ngVH P5)ɸ7kE$@y# z ɴNƐ{1QFKnɌۙߣ-'<͓i:yѢo[V @M_R:q;sw$f_X OI\d )ʹ9FPիp $Z+NSK2ng>Sr@ n[}âNfl" d@ZEዯfl! dy璋/wnkRN^1z]$ev" K9q;|Բm$Ep V̱,l d-b'cHO3/D"n_\Hz%Rd8y )geSH [,kEd!֐@F 5<cɌۙ# x晷~bfE8ۙc gOF؋pw2v8fI -${;X$B"c!/ٺ;CkYP) 0[vɵ 3>K!!R3j9'[+ {KX3Wg+m VʍqɌۙ퓔p{7-.[3-H$ W^n5sNƐ0ng&W6*W^qmP[/:.F"7=.{hnj+E2'/Z`D:iٚ@?% Ju+S͸=,p^pK0EQ9Wd )6ʙ8uو@BG\p;yR)gdfRN-}cwDNԋ~ ۭ2[ ?Wd _|:H%^즛^p68ؑTŬt60[zRs?N^C8يX{G%Ɍۙ3M?pKl5kRsLyi:-|:W8|uL-p{7ɴ$ɌۙsO:GMSeIKp:\Om/u-3!>!?#;Y` $M;8Jp˘Z+n'3ngN|V5>ܲpOtvlxwSUJa7I1̸?Yd`7Go@luʳ6% *qq2v&Xr0C '3ngrN|$^~nx^|dɯGA%PR-c7Tx#3擟'E]sW9&V'&_/rX ߪ+x *2S |׿]>y !1;*o >pKV̸ۂ%K?.0+8y3 @Nfp}$n^K?y0oW~~1CJ p'cHyC_`!@_'ӓ@Hp% |?m{8f8a8w,3ۛW]dHfK~d _|%v+p n r%pڵ (.uZ:9(0_(ɋk_& _nqQ:!U8$'3n0 |ŷK.y$ْ@xsWTa>䛻ZrŴ1i%9\VYE"pKy+𜌛"p#onN3]amKR~5.&/L˗sHًbV+|r ϗip= @N^dm0-S3d )Yøzarnφt"0[HP!7 }\޹rʰ$*̸|K@b7T@NfΒ 0[=1fda;q;K!nI̸EU-E$@NMኍX./! M '3nFp g#1|BD\Gp :caɌI +~n24N搲2Ry[oe7 Dd̳vpX_ @4NƐ{1Qn᝼9"s2v IcQaɌۙ'־w 6fyK 2'3ngnUk9q;3dpKjd@N^ʚG_'2p. $Q:q;ddp׿V]qI8Y (ዯ>K^g[-3?@;y*> *"G%pK*e@N^NN_r#9H&ȝ̱9O3I :;Cʟ|¸2q.z8 ֊2I z'Xዤ;2yQ -7_~4a%I 2;7Y/Fn<D$P@NƐ2v&J_?WJ dLkkg8GbqE&DJϿsOH z8CkrL9/pc$@,NƐoaG}<LLKrF\DNFb7#tɌx% TF .'c",13[e" d _~^d,V*#3X'3[e"D+n:XC F'cHO7ɻ~ym.4qIϩmsB ^'3ngFflfZRl+b,x̸1[zMlod )O^gŋ-9u'/ 2F v'2Q'#dOuUOfxcsH dt܏X~J&S9~xWlrl $8q;׬\R~D!'9n'K/e8MN XG Nչn$O !N~͈og?H 'dVl& TL !NxNp$+ q2?,Z~o ӊ% $/!U>+5ܐH ,|%p& J$Kۊbvld@r/W?aV$@9$('b-g LH6~ׯs&D9q;-C ӄ%$R~D7r rdG5 "Hsu.6IB$4'/Z6ic .kN#4''*n'xfI ^FG%]p/O$. trq;{o_ĐA:Y[ :96̱͆@ $DBSu&X$ɯѐ2rԱP Rv2[CIb$L'#ngxk#$@I$L'cHgy몫E ;:WdwHb'X'/Z|0i$@$N N6vSvGOIH vu2~;rY p'd'/[; ~J$@Hkkz;mVH $SvGOIH v wrq;c ;;y QS @li$@$N NƐԐ; ~J$@H_y%i$@$N NQP() N N^$pfHH@!? wHb' 'R0CʱfHH@*TNwtڴk۴ҪUG@ZZ[E`c)N? @'Gy:~vF 60<(O9;GKYt2:99uN^+}T`c)NH̓NtN͸> *JE'ӟang`c)N? @'Gy:g:q;KYtrFbtrsf3%Xʢ0QΙq2n=Q,:9 #1O:993NƐ{G%Xʢ0QYreevF 60<(O,9l(RLANtΒl,eayQYr[XQ,: 1'/]6%Xʢ0󤓣<3dQ,: 1'cKl,eayQsUߔRl,eg(O9yb:9yL'c?Q,0[N'Gy:g %Xʢ4S~󦓣<3+_D 6z3̖QΙtrQ,:9L37o:99N.3JE'כaNtΤ1\4ng`c)NL͛NtΪ//ru`c)Nί7l9U'/ZT$Q`c)NL͛NtΪ1vYPQ,:9 trsV!7tvKYtrfotrsdsr`c)Nί7l9a'l,ea)yQv2/q;KYtr~f(Ol;W7l,ea)yQv#ng`c)Nί7l9u'go%Xʢ4S~󦓣 i`!ZUNsVHJȉ.]7\ ד @Bɋrȱ$@.rd _^ ~D$@I '#ng$@$B ?Nƣ\8# H8$gHH@~!eH@NN^`HH"@'H  /X y @r <HH 9 քHd$@$trrkB$@t2 H:995! : $} cHC஻^B˜%;kB$@E ϫr"dO.zp% @ɉ @ 9?| D;X s~$@"@''jw2$@9'@'`IENN`eHrNN $ʐ 'H:9Q!9:9O$(trv+C$str6H QDVH l> @ɉ @ 9?| D;X s~$@"@''jw2$@9'@'`IENN`eHrNN $ʐ 'H:9Q!9:9O$(trv+C$str6H QDVH l> @ɉ @ 9?| D;X s~$@"@''jw2$@9'@'`IENN`eHrNN $ʐ 'H:9Q!9:9O$(trv+C$str6H QDVH l> @ɉ @ 9?| D;X s~$@"@''jw2$@9'@'`IENN`eHrNN $ʐ 'H:9Q!9:9O$(trv+C$str6H QDVH l> @ɉ @ 9?| D;X s~$@"@''jw2$@9'@'`IENN`eHrNN $ʐ 'H:9Q!9:9O$(trvGae?9s:꣎9w\d\C$ trYggoŮ]k޴aʨjټmnj!yMZX  v3θwܸ{o_UU74$3E~)r"D]aԗu_~;t\<*17 3wxilVaL"?H&q"5f_^Um H EwzCg|"ky25~:" W >cX4 @9zI5E1ߊT\E}f DcPW#nK>79l0 ?*4s~.?D lÙ3oiӺґ _ GOC";m:w]U[ DONqMQJThfl&"\1:b.1歵4G"dKid55&]LO$=:9s9i#gEctүi,K!\1: gϾIm ȅliVanف5$Xac?ܿwܧ$dK˿3/='ȅǙ? !@'v^--bjyש6Ӑ BNÁqex]+I. ̜HbtrnQ Cb'FxG!5:vnZ_Y\esߢ{}P= ?% @dԨ-ubl3ʘي ͘q#޷֭;7l,P-b ^9dȸvWW DjjҤ%FGVzi׍qV8`V!5F~u۬qc| WSӤ}Mo<'.!W>:YԴIކx,!Ψjצ DE:?={$o8H9eSֈէ]9֬YkG^VԱR_.:N> ʳmn7PiBS [5$IN)قi\d+@D G7~I "VnFU/z%+,TUCy=9s00u5iMz3ϼiV*MMMӑ#;av֬'L8S,dG۲NFoǎ80a/ G:OCɓ/1bR}t!z"@'{¥x3pz$?-2[d ڥ`.#/XrG}f\K]ⅲJmvBHM

iݺǑߒ>:Y~ʁ#P-'2Zɢlxw3jf*rRuă4D0#ɸ0<[qU"ݺ P99Roǎ0[R [9#h4\Oe eyJ81 #D&zГ~&f i2*`"{{RӯWBx`۷ͻGL?" b)[)Eqi?":[ '}^:SE iufю5 i`B5|Rpկ0d~CT0YQ'# -!cpshD8isKJ~Ddw>^?f^EDyпEWBi+qnذ * EtPGnuB_8U(.? 8ʝP3ž|[tr໾G-Uk顽! $tUzadb0氵l|Hvxtdl5w[BJE8F> ߪ b/Ixh% +- !,4ȚLvY\RsGO^}<}ϑ-}4䊋$8:9p 4iivSOƓ ^ 02N(ᡇ]tZE^^1}a֭:JnDJj"[Dd 37|LTO,/|ި =Ư~砳L'Pb#;xJۮhl2IOa*W G|67fṥ6: bXc/tmRɁftrrKaK3ի2/~*rиu5)z sT4ڶl U,lS{].m s4_JeD)>H#$59ZX8ݺS<3!=sG;ԎzxL";*L sֆ]?~>g ;- r#G׭mi:Up1V3Z23l>g7=45!\1635|)O;Ѩ5iڱCOBè#8I'@'Gϼ92n܊ENNbmIMNeHENNbmIMNeHENNbmIMNeHENNbmIMNeHENNbmIMNeHENNbmIMNeHENNbmIMNeHENN:?͘qɗLxމ'rܻ3u#d o#{ںm. Ӯ[4kӵk.:X:".@Mn-yO;,re""g)2Bw3s{: $@'ss]ۮ|x.axU"E7lP…7'NUϞ[Am \z*vo? ͛:|tu\咀t^smS70D>v? rHWsLsMLkH q Fv_'432"n#ONP?ׯWQϠml7"C[m5s_=$.c0%L-9+r=$ȂH|}1 G"QBV}fc"t9t4$@#;c9BW%h00ڴ`#h"f-Z7?bFeu)7Ɋے@dQ[=dFDaG) N9kǬ̚usZ]v'+nKCE=} W4Ob馛R1Nd'@'pqM5X -yزq@rLKNom0Oo[VWQӰg!&ϙNϐ9;_@!dK1G0= &-Ff^trHWϭ1$9fF7̞}GH-!sH5:9w)fuPfWhu yɁ`d&%@'u0MYyN;0?RNFqfqM3O])[Vpz!g 7rq0vKoƈP֨Q=l j͛wO3An Ɂ# z&zj=uTv;F>;8`L{jmnHmXP3ϼ WZhР!\|ޮ; C}R<ث\str" IuUƃeР1J/{O1ʡ8ެQPU}aj[ǂhٲ}[r=\&h:yݑ1cNvTosNN3L4]狜]EyD X<ίOzLV!8zu˺< ^ +K]v^ܨQ3U:k=lw!ڷڪiӖ x¶ζв Be'CF]QAFwY'K2 3Wxf#˽D^2[c.WPokwr iVctn}TUUO=kd++l:A&MTޅYaMY'={T@~#ā.trQy^I' hr NeZO F}} z;./ [0HNY+ZO$!4v'[%{ja2U3~S^\}9Aˬv`ݍ'5y&@'۷0jҤU*Nԩ·ÇPX7w'w괩 3$@'kI6zI81_Ȑ ՏV 0/a`^N\Ҡ+kE(踄:?JH'lwrC]Za}[WTƎ=ő͛=dž|Kɞp'/wRs>Dp2nТ{~1TE0߬gϭ\ZNֹxc::4V+9NVcѸ79zs.`ܡWdHN>NDJbo>:V]kvnYUFE۝\*}C\/N&.Ot q0iܝl\esQ0:9V*=W'd;Ξ)f:ni.KTs\FwNNT( |#8:۝\z#OWe;Yݵ6l١%Te{앎m }ZFo~"'O5lhȈcUmۮx"*0.d+Loذ-&Ωւɸi[!R58|Gw'豥-p޻ We 86 ܹVdSjO機)ꅋ}|1QƁ4mpԩW}c+n(v`w2xJDpbzkd|kA$ ܹw9rswS~/bcQ.P[U$5$3ZuFcX-oC@u S 扮`喻&PkNF Nv+#UX v'#{6LH0 ou\LħvHYcGNG|str5ZHr2B _7! pMwYR-'w3z&c Us*Nry/.jw2Vm1ue  o t8>TVX̂sew'#M=6b=02F ,3:9od]UC"HvyEYUA {ooUxa GNo|u/̌nҬ;ւD1].,kpKQG|Ctr;ݚwU8C+}]D jw~Hu.v ?-a@>W[! F*0ɄG.MUV}y qux?~KVHV&:ۇ)+^^1~1tr;qݓNc1C彬̍`ێpMUlF bbW\"ƅ0h S&mYaC88 Xyr &*5S06r]yԈ''op Mo}OӦapek? OyWx+SJN|YsqF+_8:!Fmo @Vs-5 u6ފC˘0vK? tes@* XC{$ $^ jkW`wD707d8f/ONP3\oѬu=1 \+n9rBs0 $@@Np#iz1+fQ]zl"ONSA o3?(9^ѪE{<SŘH X¤Ič@weB2A5 XBI4 ɚOncǞ8 csH}qf8iuf$@a&?2vȶ;\eDyMԃV?=a̾q#v;$@'@''dsv-9_`Tm/GMftTVƍJe ى '@'glr ?÷}lߡ}F sV{8pxf}e&,yFI @''a/$@$`y$ @r <HH 9 քHd$@$trrkB$@t2 H:995! : $} cHCNNξ`MHHN1@$@!@''g_&$@$@''͛wiNx gwN?v>h/#@Ɂ` $HxkveBnl޶ O)jҨYǎja-\@f&$@ !@'Ǿ#cbȐq-0!2W"MsE%%j&mPzv:yg '@'g' JjRֆqW++}D'PÆMX/~*mIb'@'ǵ N8ݻ ;BRO?$Lw"ZoڸرWX. OtOl>{|/ 4[(^52j_OtQ&$@#sy CE^ ߊ\*432p0ֱ8 d=m~UkgT=ȞP[r9TC&& tYwpra SȖך34P\8hRH3,ùcaHt_? _Ȫόq Lkݲé\L@$tr{aFCNJ+Zv8"h) IN 0 B?r+ŋ=ȕYL@CӦ]9f+IF0jc9 trpklc20fe(Uۊן}__8S+{'ߠ#H=s>̟5w7Oz !M0qs{ΞP0q2"Mvw K|Oa4A|I@꙾/a!>rrinų_V.lQ~hҤEhAԶGuKʃD„ 4hh6{.CUE6m^!StprMv2Vn) kYVfxȐ·,v)-/kذqE Ͻ׍(D:vEkNFGܑ9X%5GQfɖ WIbY9ײ7̚uVc7ny6oCF<0>;kO֬YrkTM[n͜<dγ޺ug f5c|.a?@A|t'^T0+'nBz{N^{M9s=gBۧuN* \:ck0vI$•"r3I0\AZ Ib>~&c žzivC'?.:OUС8>&[ 0c-|Ɓ®SUuW&:sv,;**+|M8<@O%85O{pͫn"ko-c())-5jVWϥRZa WKmeUNx؊ʪ̋S ;E~1z3cƍ:8޺;mnV6o=m6`>sw/|Zg&"9Ce&AkǑGS1sXٲem:9*Pj\[9<1 Ph rw GB'6i޼-Q;*|sVuүp[3:9ؽ+>8ݮАݺNx3.VcqoW Lв˦rTzOҰOuC+TJ͔sdY84mw2&;t0qx(U^0Qf=buZkN^fx]K]segͣ{5UBs+d7D9mڵTzXFwzw5pd-8₲98iM.R=tr^"In5W`+1j}_>B9>.&fZ7| _Nlj^@߶D&%L Ḱآfsq]Y8-UH0|S q8>ۡtB'c9s5>40GΞm᲻P6u5 fJ\Ξ @'CP-1<\,gh6߮.ZЎ;Z{M*eaٳahp!+sQXbQ'#“O55NƘ>{*]ѮI"SEp!w+Ѩҷbaʿ*^" XkN5* PAQUӹV4•YUCv J`<c,NzA2(] 0 R7 9!R0ge%n>K'ÕVlȿlܿŸŢs9IoF]7Ӯ_*XFe4]PW#Z>UeQb!^G\#GAWiu ¿2Bh2KVae9W"o,}>y}f[`,<аf"{cRsòɐ}RG]E9MgjΘmj5JV;pګ_8Cq S[CmkTU\ ԹF̅w$ޕll<Ė;xDHGQXko]x:)ѧE϶}EǓMts0u.|'EDYyaګ>uq2R"$2GTj^A"Ltr;zrsҧumYu+z؞Q]Vx%jⲚ:)1=ȼ_5w`>' @p h:)(ź#!l>O +HÇOD Bf _.́k2CNpWj=yRBY"ݲne+ǰ۵e)x}]-0isLL?#̑5Idwgܫ_U}'-J̶3+nRtr;A| 3~'3 Ep_Jeo!~{j>udl}ס@%qN,4QwZb !sC L'^?r䱶ٸP a6*||J'ǂ*tB(#|&>hW݊F #(:'֝:ƸhUWuc0iɀdqDNM{ȕI5ܑS4/[1~9ԉo/Ĩj00w߼b2m]5IIR۲e{LEPB*bO֜wQ.Y%@'gg^ œ?Q.[013&Lð^WWc>2-ؽ'7:=n쟡#LQ#޴MHLpS? J݀h[:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@JI'{%$@d}VVJ:+1''@'볲R^1= >:YNJI tWbLO$ONgecz }t>++%ӓ YY)dĘH@iNzugWRNd[HNNNdIDNd[HNNNdIDNd[HNNNdIDNd[H E.{xG,xt}|'/sOSԜJ'E@0YOtҳSpt}_>|;o&|ܱg޺-[~լMn TUN^H_S mkӦȳSzhC_1b/t#Xtϟ|3st?ᶚ0WG\qO7@02f˘$i& @׮`YgA'{*tCW>x y"~M"Lj  r[](a"'kO_Y 򟺕/o{kMlw7^i;BwYU"*Y?9IdH=D戬mHܮ[A"Dژ D59,Uuf&ol~ȘXHF: k~nKqev[\GֲIX䢺|01\Td+j* >f~5g%+H{l +Dzopq3QmH3{+ۛ;[l+HǺOQD>Xon|;txc+M~=nr)Kqv{wŭv`k:!g6ӌABy.xWiarrQ]Bqm& /J7uo/jt"k׺5}5lv7ЎZenĖ Йܱ^>:'5ϊ)I v:u/#Ezl$@gCD?fꐺoYuo5W:j)fy#3efgkV)OnsßV{@m8֨ւշwtw+֣vlX- $:3Y &WtiL;=[-9z:!ҷn|`e+>6p1^oҞ'"zΖw"k̕4R۪yflࣟ+x%hK BsCzm R x[ :;N֡4$·*&+U3Ө.tHC`Ϳ4W cxٕpa/cGGCx.[xDE3P&̪Ks=j{ ^OmryO%Vo #nqV?~IN. H !W͇0Ċ 6ӌ/H5կ>j. Euo-G]b.։f)服cEog2\ۜ1/񰷕5ڀd{o DZy|tCWoq2I% }|PX"bH \ d9eĜf"Zg}|;r1`$3L]'2]hf/D|N^@-]UUt9B6kvt"^ELD(u{ Ul[mF Z0 zs7Gu=fР0q-)n=_w]ѣ_o1x5mIѹEԋ=EIn+w/Lq=S>e{Ylُ9+K H pc-Wv?#q)/-sVO\Vx ٰ`^ԥ) 3M|gEbK󼅈7kuf]kA Wh6JCVkp)w5Vk/+J0-p[_FB=Gp/d̘`]dNf$de#r} Z+.w``X)?7gb݃u 0"%uo_0W(KA[!Jwspۚ5E^3WDcEŬE>40 ^r׊Zy "g1~뜍Rx^htg>V"bHV%fܶx5g1mn-^խ^'Mds-.AQwf&{ЖPbCV&盓}}ױG4sFЪ d/Ee2m^Dޫڶ1(:CiH 9쏁'?T1_fħ 1 ~cL0~mt_"7 ^ڼ;{H!ofxyà rwD5A1 $:ާa2Mxƾ:  yYWX?1lSF2?57y&:{qӑC'{$x& x]A u="/SQlJU<ƨ̷ٝ/&ɸw衵-trи $!ίu٣N"7ipE|*\he]?1Gs+=nU>\ qJ|:t h3|=QA'GE@2 ei<!Dwn¸*qn>v۷{ZL#ϊ)I p/~CJ>|*8 }ڶsؗƗf!fV[اAp!Ls:H0#zƌg&MA-5#Vb6ۼ=z|n6ťnyhn߾VGu1PsK 5Dh8g)K!O~tڴgLy>D.=t1^-Cu131$Y%xʊe!'?!1d$@$8|HH $q{?t1[ V'Na>$@ȃ|r]'N(b$@AȼU'Na>$@ȼxb}'N(b$@AȶI [1 @l$!1d$@$ ;駿w䠎C$ ;I;3 @P䧞rv䠎C$:7LWx3 B NHڵיxs&\/NaȤvaf;s[.}zWW7jܸym۴ڵk>}8pcƜ|Nzww?̈́# dO?uN2u!dO*HÆrN:Wjps690ZRwecɞl\*q {{g^Eǹ^QpA⮨ rAP\AGYDQ2V.deQ\KH@gض'՝>}Tw>]z%zk~cĈS7*1&Hxlv$C w7_s7pg6o{m߾-Fc6D>Q3LNM-$ӟ,_* |e 5vn<իx_h%p)/gf3G!W^yVZz=23E#ݺ=[2lؒpO pG1IBRLNK HQͱٓ&%GEŅ$N:v|3Ƀs { syُ_=l4@;9q \dF'~2eQ3:v|Ts .]4Apcq50t3~>Q_2.pM];L\U|{V^V'Ya'~]fik`9ƍ ɦ>b>U8&O9Ta1X~W f dLj:Sdë3Rm5X662clK B8`jjk gq7jtqm`Mvb/)7rao z6o~&ˀ/@?}Uo4hлh*1zt$<~ d% ]K+ǡM7u3!< e$ky3g~m2 2#9ܶEϘ <㌳ADq4kv=Ogfk_OM0;\m00tIbu6»U \p.pذu |<>in G߂-0p#7p.;7սK80=s;uS]hذ1< ԪuO/[ֵ3_zns.%~bL[BWp#L$ OsjD(eiQ#o V{}k)l`Eu'fZ0~i81} g髰{p%1$߻7nY3Y,=pWҏ>:18p4aP:V#j0h`GE}k"ȣ,,s~~X7Sy=/ bXl_P &JN6gmZ??XAx9܋*s}!kEJw1yÆ*!4f.juǔ2nX;v#}u>nY]! M\v2ь&.*(.&'%:{UTTtq$s:ʈ.F{} C F!PxCrD]@N9k/^`ZSV}#J9: h8' gl1uYr\dBPWX9~|l32#pM".*$3n4YvW_@ֳX?lٯ«@~ƛ9&d*"&“,`YYժUۢeKLxmj&XUwyM/رLehs7&Ѱ!H6I=Af%1~SUhM+UL_Bku6L4Ս8g Jdp a$;&Oƒǿ.{d \=̰! ;f l b,Ò:,3k!} )<3EV]L\o(6_UK_\~y[Lqµbn[(WLaNH5yj9 x=}Vc0Z6ٙ:Lh$@R7 A: V p"w&HX+>f09aVŪhF̢D+x>D+a#Yq$KI=ujJTԡt0)$\!L5 ǀy(1u:l6 /̇tJnƇGY5LC@sAZB DͥAz a^ߪ0k &tqW0F8n㋬ꡇX8! _aSw?: -KB%' _+`K\ t'|X5iޫ(?+I f@b:3K F]@h6^`۫/M ҸEa _ݎR5_XUJJ|դ.4֘d!Z?eK^HL'bx +xbYOHL {.ʍ 5V?TZ֯kS,ہwBUlI]i3F÷m;4gN'-@Dj|"0PoT"Ze aaF(4uSԎqauA5dE"U0RnaxB$grBd*]#1'l0M]YDvYg]`nEv>m{ҬU\0KJ*F]ܢy>JQa*5tj)嚸3(#ډu~ (CՁ燤LNH~L4yyGOLG8GF ck",BAJ|ִt߾BZ'0~{5e$cnZ_ dzmDmM#kڵ{M[[}$ը+Tړ;¡oA@iތ[} |W8A*)L#9tj:W#-mkpi602-*n$-_6,KHx.siSެYߏEٮ`®]1@BܲeG':pn۸`+lO*Fa2dӐTŬ11gˇpPdžs-1oX{/'6<y:uT/IG:ΰ0Fru peGp3DDG"L٨3u[hmNꎱ( a$1"|:Fݻ|A㜕eoTۂv<[X jsU͚ ]01i|XQ&3&90 5Lv9]=?ZْkO ;S[X[JuZb"O;@"s:ǭR@&o|FPK.XY=PDث9سn!4AZm6hZL#YُO?8;uwem sFXf}2@L# tp.%eԩ)QQ9xE@42֧ sMUL+)&H.,dE˧NtY C?Ud"8s%mz3XgBT "L½Ç$;80{L$1x-Iv'I%w@ #F|surr k1߾sng0K8dz] =t>7h:f DܤL޲C46/ ұd}emff2d/`ΝPPplѢMLL^^N2F;a^-]77ޣL5~+V:n\"-g7˫}v4]0/wꫯEGn #~ d/C+VQqowLm^J<;F^+t;'MZu(Cc=t4O1au䜚t\Bφ 8d4=,SRn&2o]~ߌβ4*?ytN{Fn&ӓGTS}le2oΝ{GY;TG[L#ٿlsӸBP>H}L{,Q;3 ăA 丸|I*@*Yp̬Ajlb2یaHφE,4,Q -_b8v0Fa* VrʬNLk&ғ,zLoO@bkLt 4 .DS 55wtkxr4gMBV`f!kh'ϛ yDHf [*&H޻df.;7b2=fQ[)|49Ζ0Frnn-T <e |44MP ?O6d wL#ىkP8|tb&tB0|&aaKT 8-]e8f a293?ӓ ╨ TXY5n9L&HcM*ʪVȀHya2={h$GfTV**ogi$:(8Aꄄ]'fz gl5FQ P@bSDEl4(% [3d6c2ݻi$9\+*61l5dz{y%*22̞t4L093לn'#p\m͙JٴoIvAص0Ρ9T&Hv`@-7o}M# ^CE4#|T 8P`A:3a$=ɮ8Ccmg}@Ӑp 9h xD#Gʖ,B8z3H#o P@?Z>gL;$wȾP*AJK+* |0dɻvH-Q9@bo1aBdr| -q:LNH`LE P GǍ# n&HV,S*,YpcMPi1d<~*@(,,c˓^C۷Ѻ T xL$[,cܸȧt21{x;T T/2kFƭ!dm4 ~< Po*P^^jhfrBy]Q*`BIY]4M|kB@b&O^m: &HsT Ln͚SDEYsbrVVaeu*@HM͝>tj&'&S*@ 9b՛P(I#¯MQ*G22`zBParBd?>?g*@lR`ǎCs椆 g4m^,>W '&1&ӓgO ͟t:L޺d^ P* A`rb" P* -^t>i$G%*ʖ.݂tSUx 4 endstream endobj 23 0 obj 37038 endobj 25 0 obj << /Length 26 0 R /Filter /FlateDecode >> stream x=10 w~a 4W8 ;)1T!wTh {x߳dnC =ɒCI ].~-ߓ͇RBi=EY!袾GEBq|I> endobj 27 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> /XObject << /Im2 28 0 R >> >> endobj 28 0 obj << /Length 29 0 R /Type /XObject /Subtype /Image /Width 689 /Height 1013 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream x|E/B]yAW{GA" ("tMz#HO!! !zeong>ogTA h@ -? B{Ax_^A' B5Ah(a y-YdчUs'OLK3d%1 ؒOA,yH @&ba 4Wc4hx)2PV(x' .L@= =XzFyWFO 6ʻ !&߹XNF((2>XAAB(wjtKuG0(V(cr]xq,V\yj'^*J򌘠N de <~B yYAkB)e(v8pzy_ < zǛ@P@=[4i6A(ͧ,_)@P: h#O>64S3 h@"E^P>iwR V{ΰmNp5੧r۽&MRou6ИmS]h|i-sM3Cl&!{S иWҜfjO |KҹxNk2ȥ6^` bKC`( Q@{!&xZs "(7jtbϗSOEFFjhv/PPz٣Yi ho  .O͝GPW䠀:<(*u pjs (sfϞ'K bi]Di >7Nu|YV`͚5(mh"՞rNͅm2Z@*}%Jȑ@tO.P /LW @ xQ@ e)xN1^:l(`BΝ;!( R% `5z>=AվzkJz~mbcc۵kGfݥyw}W礮M6PmU{ DT ))$ *g4,YrPs8OWoiA(@?_wM֭S/bxj ^^3b]_yE͞='wS1vIॗ^ =@TMz"<!"('N}&5kvkTXХ}~~Uv:!*u@*@$→H"SSQĶ_{j> ddd>|xaV}% \h7mGK7}i?'z~P%K4HBN%QqQީ<gTmCއPh@h:q y Pϡ B y7cPO д!UBLPO_rՀJ-bϦXڕ, /ZEٽynom -Rܹ(65WT=l"zL _" &PlXq*m*g1>"V 11V w_xF``%VZ,8EV}inwCbJrVKÿoQfu pNk׮ T\0@y@T}W4:ßvo )tF=v`b6Z,]t_ ޿۷.\pb޼N݄*5o]U'CP8 Ik!ETY#Ha$X ԯY`k-yЖZK AŊ5S'>>RaX{` 5}6ڜ]r*   NA\kB䴷&;7{*2ݻW\9wd!Dd˖-;x`O&D:(g[IR,? [ᮔ6P@֬ZխD m5ZlR]xt> +;E,P@3U^~;vҥKۗNSU%Z$4'ƍ$Z҂z,S t Mڷ{`e\#-?-_ VaBޭ\} NIhU#mb'/FVsΌ;l@*Uj׮]pJK,iuZ^[/֏|AK)^=k.N{Z,3-. V@@2eU7k7_hCÆvcP_m[cv!k 0'Ovcokɇ6!7f 8U^Ea΀̳mT~RD] P%Z Lm8^i7W%K6 _/Wo_{LME$حtف$+Wuͱg6 ޽%͋h)[8gϞ49A" YYYst>(NRe( P(+W.KX-( UڳyQdɒƺT֭[7@Ƥ] mo@@ޣ#SQ+*tr N vYto&/Ǐg`s1A@[wۇ~8$9O! ФW˗S퀀ÇӮ ne{ )J,W ԭ!mtRFm hs$nG;ȸ 8S !!B vPgBFhcg{zF% p-1(q3Bˠ4qI ̚6mh"?OX,-UCۑ5 bLiKtnr?8Gp]QuX \s?w h֪UIWA<н{wu':zaÆIڟ,t? =:|MW˕%{NKɒ4nJo,K}qlsfubBg_b@<ݶ@ʔ.ij!{VByg #$p_\#$X,Ydfܹi, N֤'Udeڗ_-"t*>Pl ̜9rtBΝ;WGTeVgsN?+Ꮗ Ӡ `qREP+eȻuuJ|e'"ϼTDg;Jԑ]:XP!Z`8_7=(M& BY |-\{Jp7PTd_Sb|\.N8>ϹĎH)/V#kF@mX V1$Sz,O,!&4O' i5W>jOpPM: h!mNx>"ܹs9s ɯ%3bPS<$Gla۷mwJ.4?Oه$ܑJ7xCp sH C:&R'oȧmRwΔ9J,QE-ϝ; xS Mg*Xh AX/*,@5UB tO>yޥ=Rq@*3ϰ"- k>4G]~/6J)RlّCa 7h tUT.DP ٯm08kTN=yd@O[,쳋`cNh)Preuz<L۷|QZ@;ΝzeʈH@im{? IZ퓅 @@G 3@(}gG ݶ-s^ 8@څg;{+0fiW@}ePT ԮT34 |/=XǍI[sMC(<]<ȋ{ 0_u{|;rsy DEE) <Cػwo%%VВ 5!P YBy::|qPP`ĠAF hbaP.A(# TX.=>R^r }۽<8 @O [n ' ^(o0FI@K4!P v1@ $0C]h"_OU:wt 0>P @x뭷߮HL?L@|`GCV@ӦMbH r(U6`2j`uSGlP W^zɭ]795hי',P`ڵ& @G 5. H@G MSNu.]ZPv4# ?P YCu z;H _];2 HW Ž*PXfMb쒀 $G2%t"a 8P xK(!Y ' ^ t)~ݺIoz-""55w b+#biYoI@(`Ë/n$O YަO3;;'$=֭ϝsBCS)Aa:w`>DַIP@A۷o.\Xv@qIcRR2`_'@ IqÇv>y$E8/yw/u׮K3g5/ lVH ٳ.e`BߏdH@xХt3gΠA( ,Xn^\pk+ 6-ntC&$`l=@p!,,5!!ˍ =ك>@VVΊaKFh# Km?--رk ;Iq"п4֭ mMT͞=5_>nKjմRP RN=J !y|jFF# i{̚u|hIv֓ *qrKxSg'XvOG^7G OMJ٬YB RDE/_+5Ck7G lkq AVTw6 CC[GDĭ\>av|!Ty *\z1iG 9b+6}V-]$[-7$$uZjȈA_22hٱc[uտz{߾miK/WBե-H @m$@lh 4&K@5 \|+bj+@oL"s&<<j(_Oܲ%j4_$XLb- $5᠀ \{ְH -ƣGǍh}9 Xr4$L3Ю!%(_3f/&H 6YNwm(e.ɻ|9~Aw* |Aŋj VK}}F c2$;$$teP hIZo.$[-u%Ǒ@T,W3p-xX3"d:4 :-T q)ZJ x9 c[b0"4] V?[$6GVPPTV{ V@_B)c$HʁZZ x1;[!Ճy mٳ.a $`eK`/ec0j{c~ac+z0/Cs^p΋xHX˸"ߑ Kb>!-X}T+ӬW2PN\WHqPڀ%9Mk%H#l0y l_OnC/I)ȵZP@A {]t fF)?K@]DdbyZb%ݘv.kf޼]F*BwhpQu,H=L]^+'5OiPq\J"$$#&y2 xHYq4Rq`j._FP1pH(f_< %}Q5A,dL *U3\0)썺խǷ|2\ٹĆ/ﱴ'Kh[#Y8DžH)8+0#ԿZһ粴A,)=xi Lo-YTʛ;NF&4 ڇlV%|7WDNvu=E}cvV _C̅ .y&E-Lc.coC4)?l_>!SjⶃhNZFrgf\=C\0L]^/n4#ZBeno h7pbrUj]mҲx8fP X+`$v!Hf1+iT4&{7V%Ц`weƺ4X@zYmjZИ]xx*,*` $^HyiY<[VSpO.t(f-  Z"+KFQ { g \k#.5V_۰-e H$ E?O獆1rmr5o@]g0>PfNЈѦ 4F*;i-Bݑ_*YL2nY A<\S qeKiH mn Glo>W#=jE6W[r*` $:Nضl]{ w z!X샯n8S/u~o?֘kY4N HF.W,H@,>fAŽ@}@^nOErO/v vnUDH@ mqHD)2 8H`*b&>kr̅(}AÚN&EpIFwT@ w-s?@nYLb^~hhj 0(<Gu߰@ @ $Y#+`R$ڍk.>P@ @j $}V#^ t@ 'g \& H$Ц٦$}V mKcx}Fi#A7͹} " HNsL-@5xp@=Еu},IGkJfns `Ѣ !Y> C8]I h#1@ #̟ҪL⫇ | \jIhC 8Wjl4! Hചp $}V K-L}}eR^9H$P# |  v_j{+$ @]g062ۢdS6N@!9 HPA^ă03o&; P00-Zt"H$pT;x vnUHu5Wv.%:JH$8>kr,M bY|Q@ Q$}V KSη,%Kɯ;JH$8>kr,MM{ C ʗ?@B4ypfѢŋe,_*#๔ʎR jo . X#'Rs"H$pT;x | X75@ H p@fy>r"H$pT;x |  iOw7|](E Hv>wu`uK  @ g \isw)9H$P \/ vnUHΜHOk.aPfi ^rp]b˿W>kr.5DԲ =#OV˝&$x :גޢ9ĚBtTMnƴ_qvoqeӉD" L*/OmcWTSxăռPhhD' E4I^ . HhjQ+'Ka PS^  pTrtȚ鐀7N `lSj5 ̈%`N5)3@I ȇK,3Ad2ewos*#Rݯ fDBn1WFU YL 4p !H B™ oضk~ Zt@3"K'l{>h s.@-o&M]8 LL" UQ0& $ xTvT$ki|6قPco uj |Ynm dgv H$0)4]ГIS u_ G3QS?€4fAVyHv˯U0f H@%̵R-s1V4 xߌ T"\5 Fɍvtd6AwO H@Շ8wԽ ߌ4] hZ5, cQtofH$05ТO5jbZS|6"nHu; =ܾ@UMeod\~騖M {Z@ @,/TK|i55gn@UÑT@{\USmS YZ GOX `…3hE3K(U2FKGʹ!@/@zuI\5ߛJ4- vg|"A[56lmXu!iK k@A{b_H{5z-K iW|65j hBW A I  mp@5mЉ ^W@S 5&'4q[@~cm@ ^Hೖ\S9̂(n^ޡW4] Hചp $PǢl,tBHo$v彮nxcۻ8'D $ TJnD$PǢl,F@!x1ЪRJrr5#,Vh@ Bz H1h4_Ux&Bj]*Y̍Z e؅RC$ ꠱H9( "TxBj%h!W K5naHH$QqTH9(BUxCi!t2v.[Fw$@ w-s?@sQl5P J}-:-䪴K!4sS\#:j $ @̉F1H /H 5 %:rbonFn>onDH$}`N|6_TB:ܐN=+ ЩFE R/$e T'IAxZTx^5 k*bJHjB?@GM@ @z'$ؘp[Ux@ t@ mH9(B-Tx@mT@ KMH9( < `N|9 }<'!U Ј C^]ŝA?Xs܀1W\{LHDc$@ rwH9(FQ@ Tx@̱i|Yv?qr H\G]`N|9H: s9#Ql$ #K@u,/TIڙQ7^Iv/ Wũ$ @u,/DzF?$xMՑH`c'ձkz{ @ '@Չ ;nkP_!qF $ @Ռ/G.y4Vj*$md^ޑ+;g49vm>@ @ $PӮr\CE:r l iO}04)-F$ *K@5/Ǖ#mp@eVҘCʶ˛"W $ @M/G.SJ}VL;C؍rvw{[EH$(~W[_8ʓ sH`ԋolٯy*r@ @ $b`|9TA .^ j2};+m]?@B2,V4x%2KeU_@ rwHxF3T]AQS U#QB F$pXEH$(~W͌G&Mc0(+!@F3rj*r@ @ $kiIAh.XAHBD U#3VF4+"W $ @- :3 @Da $i'9RFWp:"*r@ @ $ %R !CHxE>Oa Lc@[h62\lY96fe H\G]hn|md; S7h=@ rwH`DK;wתF6h0Z^}>44kȁFOȓ  "ÖZ[ݸۢ¡C7 4Na6]NLHH0a®]ܹsV<8mg`m_ܥ˂6mf7iA ޸tiٳ)f HvIԮ];E-^xտEdddh%$0N$$uavs6ʻ߬قA6,Zt4oH`<=Gw…->~~~%K,PO<Ѹq~m7o$$0NK$b$1bwC9͚ȡI#60N!7lN1JfVZDy!'+V_ݻ N>i.5L H`6 HMH0ro]۳碎4o>a׼Ӧ:|؝M5SAH1PRVL`H"b7?^~QFmٲڵkz$ϲ$θ9H@ 0x޽|ܖ-i^`7&MNxAP kyxfIL|rG #O<%JnB*Ut}ܹ'NHMM5"@yH`:$_~S-k٨тcl z IMv_L`&'$ЬO:i@_ $0> oe@i{ӨѼQW@ccy6fɃnjd @nu <__|m[du E{F wnx֬c=ܮ;HйsW_}+TmKҏ7x\\zq*UD="5k*@ Z$.4AFM;}HBDĿb $ `ӓ.UӍ+ WF$&&z1ch0&$޶7,otLr˨@fNt! $/@^&4]aÆ:KZu]\ڴicvZ`@ym;yvIAٌL@& ƍ3Q*"'` 4p-p'^=<|{y3]@ l=c@l93`2tgQt?m$K. !  4ݽ{w  ?^Gog̒ Nkuб#m;yf,!GدQ!A||<Z`@ztֻÇG1;~ W4rx oC?85aV:0w)otQH@wd;~ծ]{4acNhO?;ݑB?˗/Os< PzQI M6@%8u5~G1=Q$Yjd3V/F xo@ކ=~ uŮׁ@9c[D;w'%%ӽpͅs8Jl@QV3g޸qC!3\MA̯J密z% M6@W멡£P h!@x8[?[ɍnZRGl@S~簰01gllKA>UVMHH2-)Zmھ}{~3gHcEP7i::ےk.z%C㶃~Ҝ:o߾}r@?>K RG4ESZ5ԂcǎAbl!*f###:o)gT >}?C'ŀ-̇dHZhzjl[Cӓvԉ>e1K3 +Vx xz>.e:p >`樰0m1:uK< wޙ5k͛7m3i@AF!}9Y" d,9 oC?@bo`-4A=c4 @h$/[nyU lRGDD~ ٤: Ȝ-[O>4@vƺF]o).GOWC1d $0c R+bW_yGplӦŀ6eի4oP?k=z]7nmuٰa4Ro7ϒ'mFt]2II;w꒺6 xzڔy5]SBUfMɐ9r~{hǞuPHէ̛7QZ7hD?- w%9Z*qZ@v;$@o`f:`F/`,9o|2ܹs'ؽ<6 xpSC4@=6D4(Nk;oݺudg]]@hczBիWwD)?@R*%$`7ټC b * ]/M[{U "yWls}3gX֯__yo߶Kr\ˉU@ކ=~ UW( ,[jF=Mcx?ݭ[F\%7 &?ebƀڼ 1 HeGZP@3h^sOnyS І{t*`@ ^gH K8)@giuZ> GpI l xy@v;$@^2ޤʵߕ ru!sӦM\B:ulHXE$mXN|ޤ@FhBwtN.ui:# Էp~mӦ~6f H`[>F9zS}̙z=͛7_~e3vfBFGGspiu 8o7Z oC?n'h/ v@riaf'>KݺuǠNg * 4* Hസ"0S_֭}!unpZ`hY` ={@-K>҂+z ӵ"mi$]6nH>[7 $`7ټC 8rD ]hҥKL8 ']ڱHࡤ$mxXq;*@is|ȝpz e CӲ(!-(L g4nXy! 1?T]!B oC?@ŋv}~i3C2Su T]!B HPPq @~ u#fG灖3bH  xz  ^z $P]s&wH BA%()di*`ffpz@]c6 *B(Iv$Xny]mπHʱ M6@岊P pR RCw ZnMA HPPq @~ 0")S :fqgΝꦈ6  *.A(OF$˗/ Ĭ@v;$@ @T <ݻwD>@ކ=~ T:<&0H@!!!F<@v;$@ł`W h-BN oC?tޡ#ʕ+++;OaW H` @ 0";'__ ={@}R>#+Wg%l!>RPh 0"G`#?*$m[ 0"!T H`JlC+:t0" xzeW Dk&wH A*|M$&Nk xzVP AѢE,Yb #l! 9P@YBڬآ;vPWͮgH`ڄC*pYچ> 44Ԥl3*$`7ټC  -A(`R22hљ={nƦ)8@ކ=~ W4<e22g1cۺ:!!ѩ868eHny^ 'P x@\\O{ԨD@lT²#"Ҳ=O1xgHaaP A+W֯<9<Ԅ;l!z  qa[[$ؐ۷3< nu_ Tuw /Ǹ @='OXo > H 6gϦcH; Hh_bvvvttmt 9V $'g8peMTv/9Ea ͊NC+}HYw_9W%?]ǟڵkG-[۷… /_̘hHHHݺui=|%Kv%""B[nb]vm Nw?w7omڴiR-f/. ܽsv'#ȽX t')@ݳ  n:#N:SNϓ'hsYfQ ժUükv#,UnرcRrs̑%GqqqRȯ*z6l8<]P۽<?^E$^=K%$m%%zjɤ瞣~Zh"!C/.es7t+'L`.h+xRMoC.% H 3@.E\HH@?gذa=z_A'I] jv^*̙3,XP i^={)RD#lav23Oxٱc].`^ Ķٌ+W0F$Н T.jG'G?n 999˗/AܹO8axƍUmܸU+m)3Hȡ;lݺEرcR %%+?]ЮW Ė^@s%as $, a1F$GEE.\X2~Ӎ5JJ3 J_8 #Hnݺ1SJÌ ܻt3.`U6U ZS<.!_~y``Uī̴*J}R<:"++$W\v&ʳ azƍLQ]PH 9$, X;]E]vIJMx ^4@~ʽw^)U V_.`^RnH Ttw .*\vM2Z7n8ݻΝ;RH]`ƌ-b`ժU %*Zg]<"ؘ3Nî@w2$`/t "##%S^hQ<՗^zݻV_i"1H0a)͊g n)O +**-3dTTE$`*r h2dLVjVZIhGѣG'#*UG;v찛=xBDa 9w.5%:)@I@Ia>*,BٱcG㏒}/,į\rooK{S'.H VO8݉Qz.8/#>vl^c;r A|ݚMbNHl;{r V\)2e$%%M޽{tlM+;6P҆Ɵ|݄$O64.VK@H0@Û7R  _],hnNf6ߠ*UcnҤ$$>nS}Anېr$UޡzsQ{lX!5$WxUA~ү_p׮ ۷:zZI S ʥ!]r$L#G5ON6GȠĄkJH@hdaDω'Jy .bFIw I1ES`n u|ݺ޽NYqո^LLؼ;lH\O=7|+9#GFlذ͛7mC"^zI7>.\>FVH%?dA~tt0yspy @-`Ǫ3a߾;#9ɑ 24VGu\YYYV[چyҨMKp} (]:v6* &9мa"&--ndɔӜ=GLZjIQun<)!a%  9 t|Ɵ~I~Ur+ Yt4AhF8  "# 2ye`m֍:R'@Æ[ bpMFLY43H/#HH { W^Ϟ=8ԥKڗ@JB | h@tu%߷o:u2PyH@O_(QB|TRѨH0:uiC-Zjpf>$Ӣ!CvϞyGfUB W}dЛ7on7r$uNR *HI 8PyBqI3 ؖƤc.,Xݻ/lzVF >e׎wtŊ{:ʵ3H`[ Bb:_mڴz*V(rP<;P. ȟb>|ѐp%ݽb !!tСw4ֿuDR+W\zu hС4{?C5Ĉ ͍䤤0@ bsgS&lyǎ5#Ntホ:=L.ЁTVä \ RF qqwϘSZ\]LQش hb'!<{Z@vH$0NMIIOQh(;N$0Nq$ 7/@v6q@dSV=EawQhta׮ >a%Ku14v\\Jx#m_ r[ފ"Rz? /g6_y@H[1,aiO?]MQ] ~ĉG- ٽRX؝wqngow>|2p%K,ٲeKLLyր52gDϤ([) YZr1qи wQzX7lɓ.__1wS _(`r-ZD["i)HP/ #|c#*x(l. 4EZв^ΩS{922.!5+ @T`i'OV1GR;Ssn$&8 @(`& \:/+?; Y.ن@3l @(\Tː!9>X @(̤7!ȤmImf*+P 7!pn BeYQ…P @(`& &Ʈ|}f*+P x7!5~~sQW/-iF"mjT ;,B,-: HYP @(`& ^,NE& TW(LoBYl=KV<@_1P xզY,0tH"P @2$%@P @C+eH@+%b|ϖ.P x!}QӃ,*I5C"2+ S]P۽Z*#`OT] O|#P @(W"qWgڵ U 0+Mfddj$P x^i3/ok4u 1eQcU5 ..ʕvn!KP @s);Hb VëU8'Wvg!?P @)$PU -[EY,iKJ 7d @(`FrOKoެP8yCN&UH@#򬬬^.Y2U<@(L@n@݋z||'5jL)\XL=d @(`j $з_pSOȓG"50};H @/PH 7n ~˖=',xË۶mCP @/PH@ S?  f5wtޡ@n@|vv]* 6Y,#>{H @(M 1@~HqOLLl;uX7kqPȍHe|=9o^ `ybiPCZP @SH@Cj/8X`Eo=fCBP @TH 7n 6U`yv,_^!(*$PU Z3쫯>Ow SyC(X +\+BJJJuG+f5"u24k9P0ohMwT:^ ׯ_kc`:/ @( Cl{gmrvÆs4'utؿv4Su8yd?gk},) @( hÆmwYZݨ }=`@k $Q#V/_N`U'X2_C !?n],hvv&L f²y@+Ũ!C*1X sH0boYխg7m8bmt544Sn5s T5l]b9j!E{縉CD55*TP1 @(k 8EqZgYW%_n\rHG6[;ݻA* pnY9DlP @R N VeZ*Hzڰf 6^5hFAO>VqwefID dpYW%|W9{6]1 J1o%J9|) @( {wōT 3׮\ys6Mϳ?/uV~DP @Q#FkzV\tY}L{@uamh-or3{6L"r(O)`~^A: 3l$]BCC/ݝ7Y,? ; @( >5t-f5hc_ݿ&MpFb?wD2@ {_NuB u3ؠP=.^=~i%7,fl~ٷn: cg< kw]DEEy 333~h<@v^2 4;8'O %9xіl覴Nt֡~Ա#ԋkvu ',*&WR?hF۠A^xvfJ2F)J(J@ɓ_UKXX(KKᆳ5zow>@Q-iiׇۿ`…pi(J(T+l 鏚`AZڽ]}ꩣF47bφ[5hIJ2)(JH) |[;:Uͻuׄ5*qQmY>Xo)J(J@g1v Hۓ=b(JbI͚=_PAXs}y{cq(J(YX-8eʔGoUӦk׆b'V88iP Z0AQÔ%@ P@*Hbر^sM&M:׬"ؠ -U&s H \ FBpN(E(4$- ʍ(J (--㏷^xEzkpxy3~C?JWšаPPI(wP(tC(M(}q5u%` l۶_םu֕wQcSؠm:6c(`K`i(t[(od DgB#}k%@ M}'|]FWcs>>v=f쎙1%P% ;;v!PhLoOy-nmp{c'Dm GW~FA{_T`e)H`](< Bp%@ Lp|4z軯FתY}FMMK[vpˏ<du(KnN/x% ѤI~ou7^}n΋/S[}t[z]ްm >\uw{ƥڳժԺ5 t,HZ <#XTpR(Tb)J ()) GLV>}ȑ#هv|gMbb@|߷D`Q!d~aXj-Z-**Z|~oH9-[(Y׭[׺j}3g@>s[oU9(%@ P*  OVn$XƷiӦ&@Y#رkZEOc@<#n$ qGq&ex(Jބ"APs%# 'p]poqv5j?>R^9}۶m 0&0fW^]$8Sʔy һ<>`Y %@ $&%X% #A"Rȧ(J %s$U1!2;->ӕϟ/6jhΝJ9o"qnSVxȐ!"_r0aNz~-P%0ž-IbqH V$0`0Әc|qwU*QFo6}'Dׯ_߰aC`jզa~X %@ $( !3,X?6e8u[5 0NyK/Q8=s %0N?nϓwj ~"%+,]TXd8 Rn3g(HJpe`O#˗/#LeAnXU;w:++ oAZv?փ{BB}B!}_(a]laبg9CP^( j a@PPPPPPPhD f* X@9(8cO=*ذa=u ہ䈔!\D? VϬ,{4Wa6cqqq y0hjߤb(98:wޘA|U\999"cK_W⥗^1äN:xdҼysuϜ9~8bGXLSJ]!#k5 )2@&̀k?|juaa jnM$V&MwuoJvH Kng̘qW|17?|J7T7V^GiӦI&R714"z͢&< +h%pL #`K@Fxۺs_-C~(RU +?䓑S$C?iBC(R))~VOHIN: k:NPb;_(Wa(I|A$K.ĞuUV`Q(t7ߔ?E.}{0UVGļSO=U>0̷%2ĻʵJr{1 9# , p/W2g4x%H2w" 4;!OvC7Ajw= DQ6o*  u_ ,?DT.r! Q.7qTn?8B&$ PtaaL2%R}z- !믏=|dbӞF!:(۷Ho~jxI(Ob|FPv`,ڞ8 @;_a1p`/AXb2`ʪ,w{W-[q$QO+A~ܬ|ѧOۢE0܄PV 7cƌȰ|a^ϗrG«9ƺ/ y{ 60뮳r##/% 0}yЅ!H"?0PaM9>X{DmۆOGe${ݺuX*"l%R|׌^#[#{OS9U JRсۿ) z{saA`BΗəH?ު9fMAo:QH"A$8>c -n+CuoV 8G|[G%FX]!&ay0:m%`c1i0.HaR>5ydq#O  9E1&3ũ;ʧ Z. SAPhYEJ`wv,Pćyg}G%dff#8!T!#(gQTҪ0r⣒QæS=13:ʾ/),Zm}"^>: G2FLJBa\Y$ # <p[a[e#LPz,ōFxFH%2KD`Rߣxӷ'PȎdWp $>\uJ)&c2Xc^3R yolF$P(5 1|O@4_rSEYL PTÆ }X9AVRc38#9 /^ֱ9-\xLXop|paΰaÔ: 钨Ev 69#*%^A]kGr\fUبNwZfM9~jrKA7\E[FJT8\Cv(t yq}ŵhMnEzxUH4Xҫe4 a5`P ?[rvIPu,|ּQ(//a("9tavľDÇעav\T3TCFUUhb=29_^w|G2BHMLB b%& rhRZΝ+Jz \8Prf? FBp_pf(tN6Un`2Xիwa߃cIV !V$xgD;HRk=T):H)K0.{'@`,YTذl=ή}Y9r$ .a\Tz 9g/(vs2DmϓG-|VشgE!*]X+`~kLO T A,6M ӊ4HEBQ դI45\TuرJJ@8j+gO?-Ge$Wyyy-YdH 'E"N v{{њ3sJ  0/C[ۢ".;5}an_%x[y0R3Od^YYwg `59$@X(V(7B7l Jp~'gPCJ" EgH"4H{q݌T%q^(' 0b&<: P%PQ9/v(v&purb5GL}Q@`%mVu@@I!/Sfl<NVp ^x! DXȦ*~{(/%`0|*+% #=qcɊdN Pv M9?,?H pL9fuֽugD`"Պ)J(J a7֥֔xUerSf%@ P@T ̜9 {|XpuaPfjQk^e`$L`i֬6If؂C3麵%\O4&x|WK|0 Xaw]ζ^#c0`K.]30XצɪI&Xcz Ν ڴic%.)6x`'ww/ 0 rkР[>t?P"]&N@ m澪7gv\O<<&DE R%8Äꀬ,">/na3 ͛!P0iP/f.)"CLG}$gJz""F}M81@~Ws=|Mة/ՍQ)+֪ݘ'p֮]1lu BجHdҲe;vQ^Ү];qSWfʔ)"CLQ!-<󌒞HQ$nn֌ R(J&i&9©!>(Y|\r}Jp+,2 /~7f(S}Jz""Fe]&XLqS%B PyHA Xbdwov`XنXXIeDZav?z >Up'(a XHY1O=_|1f,~ᇘ=oBi. K, `@ՈNc#1}UWiF'D/b%&*=fE/++s_y Q3W,Jz""Fo4X8=?&AJ0$y|[DcP(A+ڵZy 0q䌝Ds5OB>uѣG닻ꫭaΪ>%Rz ǺPub"٥ʕ+EFID-avhث`#8 SNd1!L7ŴOQV.]eU#,,4i");ce2}tqCqD₂,QO<42v͞? ۝ؓYGڶmk%F8'|bd&' QYnEHdw!F&XgA8VȄ1@08 Y(1"=bJ>ݍȊJ@ԩSǫWZAqQr(FO:$ͨr %3Pyt\^!u2rFx @Sq„ a 0z'޽;F^ YIزA[,  VϜ9̆>oEUXrv&\( Vr4ۀsp;iE;C<T~,D+Z۷RO?^"FƬzChV)QJ+%yf a!2XQ̌?ba@@@R@) 0}40~af}DIeleX.z5נ cƅ^qLGZ4An^7|"|/?A@ ÇG-ΞH{\ xHQD;$s8,)=xxD a]tEq\(#P*Ѩs/E#F9$ߠ 08 r5|>"sF)J ֓# 6 ay86'KEqq馛GPFRia8cL](EDveQpbk QHT?TXVr)JtD0J $ 8 νc&;>6C蘀ذf,˭GĽ}}S \OkOYVY| ȶm۰ 0 bф0RŭHixEiӦrGgJ@s-xu#2~a-j1/ELJzdNc ߲ج!j> qɒ%ZxްrnJd&}'GoG|+$bWS&%L -w0UU>O kY ̑,Ͷ_p-_2#%õ ;sw`^= `@=hG${GEqofجDX=j)JH"vIT YnH@<Ս+H{wrٯ;cgqv{ iL {7Ř6m.4#^.&Ϝ{VZ]I#J i~VL  /  Q}9*a)H%Z% 20I^]c&!| M0p#Q4Gm,sٺukL Ĭ{,5^.{a$\HH0U@|pn ohoÑҥKecz(N0^&Z+{&y.t<=z4$K=)4]r%ش#'ud} ɪU H &8Zwމa B,o\ l#u 7 ]l-[׆ EB k3*a$3f)J y% {yrI@ǎ]6nSwaH '`(JpmAjo&M]IGv~D'cJ f4Qt9 o<٤XZ "œ)JjK `mOU[\;$+zƍ1+G}+k/3s]z:~k-[doՋ^W͟r޼s7{߬Yٿ3Ϙ߲6-sڴS3LIoҤݿ_~oɄ ?dEo腣G/oF7Ĉ[ JJ*MyR&$ys_ң}}YLٯfק_ѻ7;Ы'3zٳhOGl{gYݻpf9 #7g9zYq{~z~s{_>׻=CdN~֬M._mݺҭ[+*4!jI v۵ߛo_w3tha9)>|q'O^7sf…[׬)ݲ|W7Z;_ǎڷѮ퇿O?ڷjfGʝ0aݼtiѪU%7n^W}R.cb·N~@A da<y෇=g!_߾]oa~ [ؿ=,8 R.}=1hP?e2${տnƌ Q^zuɦMe| ,>t^DS__}+ {Up¨>ؽܡCW֗R6mÜ9,ٺbbt5)ׄ)5+ D;lqk>4ڬ|c&N\?c 32alT7)Cba{rJ2O??>#N^h8]c˖q B8Fa0W֨?أǼ9Ç?'NssFFQnnܹi&xQ@n"[_}'':wg% [Fa|m׭ہZtLFr@0d9k)-NA},^qr N N.I={ʜ`)=_|KEǘ/>=޼˖\}ÆtCQ@V ~kil׮`Ȕ414s̿FʪΈK$#~^' wG^v(t5kՀ?IYL=Dيo5?u!_y寱= cMzSϞ]xKm+ߵ+FL B" Nh~7'`<Х޽m- w5lx撚@Y9^}u8/8wvsuxUn2 Sϙ(X!*!&2FL|7bkA!}C?r c>dJ> ea2 j֠.YfXZtCr. lE7dN3 a,(lQݾqkM#9 35DPtc27ken{7&x[b@V!VuUOF\Nc̘cǮ?~?0aaҤ Sl2`ڴMӧo3fΜ߷̞uKw,_^[ys”I, hr7ƊwftV1bYj8&Ӭ'N0y2ZVԩj5kYEs͝myc,XPp-*]x'~Kti9~˖Un*+32%ȐPiqUL*!2'/XߺaZ<?Zt֜bLeٳg"9 0?!C[P`waU ӿ=kZŭXÓOcƌsm_4|7m?3S4PFKOy1޷ H H 7['kGڿ[8p[Cjt+VFjm:$ Cva!sy-^~e=SEl=ĨXݸ`_hY%KvҥC{q7.!F .vkkK B"pa" Dl {DrbH|P. _O#G=:g̘UGUpI4iR>U1UΙ]ְODYIa"D`Xl ja"cG1/ٟ' F$0nQ@Vd A"D@V"A0,Z dO0@ &hHh 1FDE:J$tDEm!(B$6^ "I%h:y"D m!7(Hh~RGNH6@-DE[kA$P4?DM'O$PtH"-D8 O(@ &hHh x-'uHn 4B$PH`"I%h:y"D m!b@ 4<@m"F[ `@$P4?DM'O$PtH"-D`Xl (Q"'(M$h @"qc'uHn 4B$PH mDE:J$tDEm!(B$0nQ@Eeryv;wm~ʢucO*9mZc_`MYa @' &f羽ۇ ݼ:#K;kEmqg_pЪߗSOm!( I$6^ "rtf֦N_VtӁ5v{<㈜r&~Ng$j/$Xg)~~fo C$h @C"qc[YYz@ANs{K0,gA؜$h:yߐgSk#u[邌RzbeN$h @@"A0,Z #s;Tq\f]{]UtJFDM'`d5ZTòo D H`"cVddWrFDM'o oFxD+9rTm /Fv?ZO?s=r>0.bʜH mD.>ɜ͏ӺN3ۮWFe~jPQoDC%sb)&b,Q*pyɶ1^k*o} `@$Z^4m)<-%DM'o НGאH9T@@-&sG_SI#=kq|;9D`Xl ἵ1zuuxA* 4 $/Ma T5ʳv%h[$X]5 |$f~RIVC$0nQ@j"iUZSB~!ta" zUQ FZHі8`^f~+Om׊H9 a"5f~6He ]Gq" qmqlW 4|f:`/ ΋Wʏ01F)8V"+" LP O({!h%$F\ޫ&W9 a"y$9-.08 bש]^Ny " ࠾Ú<ydңYސ/DM' `'ʳUV[Yhʃj׮U~o|u/k k5|G$kzfskb_sKJ:vp̎/9so' `H q%h:yO%_~X9'P.!h%V$`M;SUUH mXtuUk 4WHvӋwrXw @-"V(QaFqkƍq0 HM$%x-ҳ9E)" >졂WS*"F[bE:D}UH m)#ڑKUS!h:y{:#Jr 4+`Od~qB"qcR gcNNB/|y-ބNC$xMh xu"F[bEez x-R '&·ly j/@{}V)(ѱn|aHі8ēJHdО!7( -\>7^8<ߓFjτH=DH<}V ^? 4Y>(ŝQ|AB"A0,Z8L΁Kyebrw" QUQ2Ǿ~7nݰ_H$hKH 9vX){uH`Ga?-MyBC$t#&h}Hі能lq+C kA$@[7ȇAknȱhDM'9@3KN.9?{{^LG%h%>$%XrOanoZ&D8 p%ő neİZ|HM f8T%orƧm ;|&;TЈqb 4 fd`>m ^P%4Y `@$ 0m0ITn-|Uas6zH}@b*W+Z0 k%FʜHH c7 \.3 aׂH t7y?޼zemg-);Gj 4oH`=xnûoo]mWʃ9r+`!@-B<ϐK?g(YB$0nQRx+c_gmeY/<"l r> 4H O~xa0Xz`&.-ǞرӺVd~m 4=ʄ1%w5t8~ "be8"O}n2'h+$y/=&&)1G~^w-1FDH0"h:y"Dxk{6W^)" &dܼj qmYrַǙ9g"F[ L$0nQ@|{|#t"Mӝ^;B$t> 幷>Tr>鉕3@-DEkA$P4_#C_q`|;گ/HM#A58(nU_xe mH"lD8 ?;yu}W@ɛC%7bD0`Ham!(ZG$6^ "(iu y<87k< DNs$x|cOܓd8e~H"iD`Xl X0c/ŠQhYr6f,(}HE9)y?p$""F[BO1FDK>;6?4:Q" Z_&W뉕@-DE͈kA$٠{2cY;Q" *=Hp$r"F[BO1FD(ۮǎ)/Vڝ(@{GӃ6O9\u9@-DELjkA$z=vL}7TiznDM'"=O(H"+@$0nQҢ<ﴝg 鹉 4WHHyTnD\K$h @ bE#\[YD0OaEi}qGN+$g^FX m!=D88k{+/(@{pfJD-pǭ$B"F[BO x-R /)^i}qGN$Q tJ".$hH  `HG>t"P%h:yO JP+n[IąD X"A0,Z8ԨOo}8:(/(@{mxZ_ƭ$B"F[BO1F3 $@M_H=A{{ڻZeuL.V$ܿ*:`lzXl|Hy 5nP֤Q١<cJG "F[JD$6^TFa9=g( 0(@{Ol|vOC䫈m!Ȫ01F?sF{ މ7>|دj\^7vzkTvX5ytc 4'Hi]wOte&nkW 4B$Ua"A0,Z2Lw3{u_H=AoV꡻Ǔ=m!(]1F#!اr_DiEN$x *"F[ L$6^TF9lߟ+%bΘ 4'H$Ny= ;1n% 4B$zbƍq0 Hq$HiJ%h:yO‡+[IąD X"A0,Z8[tRZ_Q" z`?g~F>J$t#m8dM9"F[ L$0nQ9E-v4љX4:Q" {}V׼ݫ@-DE[kA$?yBaO)+KJQ"7xvV,FƑ'ۯC%Y 4B$zbƍq0 Ț5+jyҍGCs 0L$toX鉻9m"o"gE$h @VkA$P4S 7,kJFN bYwc㴟WxJVD m!7(Hh>3 AEb`M#ɺG$h @VkA$P4" }{00>;۞#DM''G o߼ ] ?Xorpy|_8kD =$7(Hh~蒌 @P+8EAɝ2wH枵m!( F$6^ "N^9 ]=53$g,(wr4DM'O$PH"-D8 O(@ &hHh x-'uHn 4B$PH`"I%h:y"D m!b@ 4<@m"F[ `@$P4?DM'O$PtH"-D`Xl (Q"'(M$h @"qc'uHn 4B$PH mDE:J$tDEm!(B$0nQ@ 4<@m"F[ aׂHh~RGNH6@-DE[ƍq0 (Q"'(M$h @"A0,Z O(@ &hHh 1FDE:J$tDEm!(B$6^ "I%h:y"D m!7(Hh~RGNH6@-DE[kA$P4?DM'O$PtH"-D8 O(@ &hHh x-'uHn 4B$PH`"I%h:y"D m!b@ 4<@m"F[ `@$P4It~Ǝ 9߭q+e;ʇ4DM'X$U8jŲ>=i>胾"F[ aׂHh~(:޹|-kVJە8*|˼6 4H03kkzGӝ(^S^s+,KP hHh 1FDE蠕z[X P:yEzە`%+DM',ɨ贮?WmW5QuZF|PQ@-D'VH mDEEǕs/&jgIpbV h:yH$ݵ}/wuWWQ" 0d [R@-D'VH`".k3-\DiqDNC$fP'k b\% 4B$zbkA$'rnU|< 4WH)˚Ez{>D$h @鑈ƍq0 Hq$ۺ/+}HB vMX^IDD X"A0,Z8W|ov2J$t kyJs%h:yON}P MsǻTq9@-D'V~H4}.m&J$t J[ ۺZ"F[ L$HؔE)H~; h:yHJ~,{H"ocR zNZċ*MMH#У>PLZFIĵD XbE";k{{G-MO+MMH#AmWF}>$~L7J"%hH  `HԆ}轣qE-Js%h:yHptQ >]% 4B$zbkHG|轣Ѳl鹉 4{$YQ+!AO(H"+@$0nQ@"? {Xr}+kW?;A׬`uR^~5nXִQ١tgv WEsyJs%h:yHp@AuZ1t;mը,o`3^Yu&;[gn^zWm!( x-R V)]4=7Q"wGiy^˒#=qDZ6&-yH"oD8 Ǧعeg\?U 2%h:yHY&Ɨlʹ&DI$h @ b@|Dggcb㎢on {)& 4!$c5\^'n%Q.YY&4DΓH"* `@$P4ߊbJ9ɻthߨZ~jDM'o 6s2!s/jZX @-DE߈kA$P4_ ԲX=Uzt97DM'o ;;sc_ */(>-&$"F[BO1FDEFgw^[88R^rۮl 9a3 @)˙NZ$W--mBD =$b@Q^;wGkoܟG,U`Aq( Ng$fIzk1ɚWMQH"F[ `@$P4?DM'($F 4B$PH mDE:J$tDEm!(B$0nQ@ 4<@m"F[ aׂHh~RGNH6@-DE[ƍq0 (Q"'(M$h @"A0,Z O(@ &hHh 1FDE:J$tDEm!(B$6^ "I%h:y"D m!7(Hh~RGNH6@-DE[kA$P4?DM'O$PtH"-D8 O(@ &hHh x-'uHn 4B$PH`"I%h:y"D m!b@ 4<@m"F[ `@$P4?DM'O$PtH"-D`Xl (Q"'(M$h @"qc'uHn 4B$PH mDE:J$tDEm!(B$0nQ@ 4<@m"F[ aׂHh~RGNH6@-DE[ƍq0 (Q"'(M$h @"A0,Z O(@ &hHh 1FDE:J$tDEm!(B$6^ "i[9[k{v\WGrΤy3pH 4B$PH`"as;mm?mWZ_g_†G, NHh @-DE[kA$P4_bLཱུ}Zh4<lGv97DM'O$P4H"-D8 ͷKw}f@c5v?ꩺ ^x~ƎE8H$tDEm!(B$6^ "^@$BhP֤ת_8B$tDEm!(B$0nQ@.k?H=vU{nûTNQZ'Q"'(:F$h @"qcW5$q%GNH(@-DE[kHuOL|GNH(@-DE[ƍq0 Hq$88'R^~f\FNHh@-DE[kH<1qgI`ݹ 4<@."F[ `HƷ\i Js%h:y"]D m!bE*#;NIJǝ~xHJ'O$h @"qcR &dm=Js%h:y"ZD m!bE"7c=qg퐔&J$tDEm!(B$0nQ@"뾍ێ{x᝛4=7Q"'(E$h @"A0,Z,gZt Js%h:y"ZD m!7( e¶qq½w]z}_Wx`E̓*Y^V!u@ݲ.RiznDM'O$PTH"-D`XlHY$;(w=39KtDQQEStZ]SQWAUvU.QD>\Dr#p#$@nFzJR꩞~']3]UO?UЎ좷ꂂ BkhIz \2|I 8<  X lOzɻKH  ¶H0B=C}8|I 8<  X l=9f/I 0p$`$c^ "A\zgGg/I 0p$`$>ˑm uBfO]rjp:y c- -@-@9Flᥰ37Q[wdڝI 0p$`$>ˑ qa>;wp:y . -@-@9Flᥰ3zs+]ô;@N'$`$HH |0##iA-it'NH Hq q @[x)ld=>Zܸԙ I 8<  X n-sZ gHHǀ  H@_J=ԟ?eQI 06p$`$>ˑw|Ak6:'2m$  8n0n1b /@u >Zx  8n0nHu=A .|qǮZmvo 8<  #R f 0aM\LF"@N'$`,$HH |0# |J;*aĆW?a.">p:y c9 -@-@9Fl0W+nɻ}^ Mp:y @ -@-@ჱ 3ɯnZ@%DFPz22! $t@Ɓ@[[rK$`hr箺9(U1+ҝV]˯Q̇ۺѓ!  8n0nH8_O^Hwlyϳ7rvW9ʄ}NH8Hq q @[x)-p:y m -@-@ჱ [: $t@@[[rK$`o$ o 8n0nH8I 0p$`$c^ |K'NHxHq q @`,G@N 8<6  #R [: $t@@[[c920ηtH  H8I 0p$`$>ˑq@N'$` $HH Lj-@N 8<6  X -p:y m -@-@9Fl0ηtH  rd$`o$ o 8n0n1b /q@N'$` $HH |0# |K'NHxHq q @[x)-p:y m -@-@ჱ [: $t@@[[rK$`o$ o 8n0nH8I 0p$`$c^ |K'NHxHq q @`,G@N 8<6  #R [: $t@@[[c920ηtH  H8I 0p$`$>ˑq@N'$` $HH Lj-@N 8<6  X -p:y m -@-@9Fl0ηtH  rd$`o$ o 8n0n1b /q@N'$` $HH |0# |K'NHxHq q @[x)-p:y m -@-@ჱ [: $t@@[[rK$`o$ o 8n0nH8I 0p$`$c^ |K'NHxHq q @`,G@N 8<6jF1;'rKai$7oiú 8<1 *3QI X ,f۹6;OP1;$H4 #R [: $tgkx  rd$`o$ o 8n0n1b /q@N'$` $HH |0# |K'NHxHq q @[x)-p:y m -@-@ჱ [: $t@@[[rK$`o$ o 8n0nH8I 0p$`$c^ |K'NHxHq q @`,G@N 8<6  #R [: $t@@[[c920ηtH  H8I 0p$`$>ˑq@N'$` $HH Lj-@N 8<6  X -p:y m -@-@9Fl0ηtH  rd$`o$ o 8n0n1b /q@N'$` $HH |0# |K'NHxHq q @[x)-p:y m`ΜdjF1;'c92(.>=yrm͚nFڰ@B ØEE's._ 1bQ{ -l6K& ܍s$`bPUXxjKR@k 4yde[ m$ Μy H$ Z{h $f6 'Z @fMCr)M H4$ 9Rb9*h $0i$)MZ 6k&9˴i0n1 )?JOZ d+ˆ 'M̽ ͚Ⱥ3oOr ?0n1 3Y^@k yðd9Y3iR]z" md$ Mb y[G݅V 0@jS| 6k>)j  3;vY Z L,.nT$жY 9F~.cJ 3[?l H @Q^GY6K&KШ ˫w  E?KN@av}vpʬɓ}X $жY?"J  3 Z 67UwHK@m/JJ*dW$`w$ ϤdKK@aM 6Ϸ0$жY#5'HE$ $$엓 Z l8sd Hm2 YH9[$A?앐 Z 8=ZhlT$жYIZG^^o ȃo%[03@k =}~O 6+$HE*$ |N–Z m;wt @k G|=u93Hmr"yI9[Ê~^Z 6g6[ JLdHEf$ |Fv/@ap73Lf $жY>}=>=qH@F׿ؽ TdfddWׁ|-@]ݙ7LWm69֭%wNZa?H$PػRrP{o d˖ܓ' J(55uSgi$ AV[{w$fhk *][VP[{%S/T`s$HOHI+/(K3y&> ;ZV&&   RR7m*G~1̶ESkWWY$(T TV֘ o;vT0?| Qeggb2;lɆHfɍ X%)/4)vC IH-*M~-ջMޞHu&!`߾{–M35lVHd`${؆/+814*lT[VVHZ"9;v>\0%3`]6A*RRRaluv@ SS**jFh2洷$z_n%۴_`$#Q 8Qn6ԇ[%,\/LH@ѣ@~e̴@EFj++PXxjqapd~6+cyO1s– ꃑ`${/&L$&mnհp_p9 ʘ aK7WQ0Rii@#X('+$½{-K g_,Dslii6@NN PXx\\C/8} !^sی–:[WHt88/ _$Z m ذp_l6{zA   -&.lɺH``[Q`B,ayXt/5k*4}e_ ["6Irx ر#{C ϵIXxCdL紷(_;zW^)"lBH@3!k@)k܆B _& ri?}z ,W ʘ.:TlK t;ɪ@Fwc $ h%{Diӌ\BH@"xeS8@aKM.9N{R)v(* OlR(* qs[ 6n,EujwaaK"֭4G%Lp0I5kr i !ax洟2%ѐ'#2n]>cp6>QaK"AFF%ll@zbߩ@6$ ?paFZ6 5$=( [ 6l8`$ȩ? AFXUTL3b $%mv\d?)-,  ^W mz S}A"{1lH`$؞_"kd/ N hsˀGKazKؒ !Ν$ XlwT/$ظa~qQII` nR||^n.^38^-˟ߏ`$MdS2!3 ֯GXSTT ʘ/ޅ-4P|<@",):"&e Ny1OHPXEAH݋Eؒ9H}`$oLE6yD -Dm[Qcg~S 7S*0 XeeB 4lI4 %PU`tT  +ލpI>r#*0 ֮-E1$&aAaKqHF*/a$ ;7Mgl78r}}B?F6b!,Ҡ4ÖD 0Gek@3 :=T`8 , #I 1猦K3lX$@0R@5kmT` lZ{De%)kW*0 蕱zeD2=PX0($`LQEP̙fklTp[=+E)]ik&lG$8 x)* h%T{|Q<`c ALWPP.(G ̙ [ ().`'{,;;ۣZcg(`](Xkڴ䦨k$ /sֵF#%n8 i[T'&bFtGgo{ h8LHH*"{ݳg*@sfR $HI)FX^- =?W1 AW}%QnQQ, i̙Jebppnm۶m*P]]7eJ#K#ycf*UF $HK;vm>ϨV_;(oh>2rM7˩3JR=Hp.Γ!xHƍBݗٰfTQ9*t,BQ |hc TTL>WAXUPt h[ʘ"iÖ~󀒃~}jkk]aanZQ]t租~Wt ])$zm~>}Gn-CB2fkSkjQ$'iΝݣ$l*Er]ӭ7|48 Ȫ@iiJ iơ*Yr;'O5 )$A_mꥑ33QC2HT}DHK.r10yFQ)J_Ν:< y( T@w4VA@,mcM!EҮ݇sVUn*R Z`[}X8fԕGa/FZ>))K(l)?rX؟=G7W!.3wseC&P ȤeY4_I ο!췛nCL̴)S$bC(Mef}=;d)]TԄXpqz(PHh*:ٯ(FFq:_yZMȲ\P @(0h*p ᰢ<ǎV| @(0F+tnZSaa##|cJ@(F+ʘsӯ6w޽{P @VVeQQRQ ip ӟokq<P @(`4_tLe4ziEyENN]7ߜa\Up&(P{^~7<ʸQދ+eNPЅN-Y H(Ph*Ub EEYt}⌨P xMeLo 6;v ݡNQ>SQQw_j71P @( T-}Z rٳLJ:P(Pch*Me5 |(97]uUjjuP @( T:G`\ @(Rx? Tt^A()@S* +.#NP'Snt7))ɧ`(P8h*㮦Le }$&&W @(0@>S `@q (Phh*¦2Va޽.;P kF@ zņ]- @(0:lyH(JFXqb(PNe|JQns8 x)c@(b=lУkш^'8+P @(`43>Le|T!!m{nJS@(?oXgn(OGDhA1W&? j\C @( T@6N縱c_pATn@q (P8S=c3@0 /P\";hx{4*a39s&33u@P TƗ蛷`o4ժO0P z᱇OթƮ]o|r)B(T T6* }- +eq-TTTH%dggϟ?С^t\($wҵcG T| @( OIMOe솁VKG6zm۶]ՒI2,2+hfdsss,YȰai>2rA<>hQl(h* 'LQ$x 85aՍهgelXإ;ˇU'i;0==]=7ʾywaaDEM N2;gźP @(`ulf"  ݭ]O rEipZJ2IwHhmq/Կ[7WXؠ-ZUm(ÕEvS@X1< { )J9?O uM?>}GG(>+@"<C͇/P ]PO?9s<੧(*ޗnõ11ڡ|ZOq=վɐci҆ٳg?ptu RQVo5x!IP TޑI(ڼNSkJJJtV݇y siꀇ#"Sx]Ow3tҳ +SA(0M興B~>zʏEDȑ#&Tѣ/y}kݚpT8(h@^&TY@(0\/TG:I!!Z|/-#@/Tҩ~EM:uQÆ }^۶[w:@g}ߍk_^q @( M\dٺ-[bfP@*vqQ&lh͎C$г &ՅFaP 91ӹfcQf/N.Wtx _>'+ @( |7ƾfgF :sRQ U=k./Q"A(Я@aaᝃvvoP`EA 駟4`ÞP @(`!~q;kzHnI.r3?',df @(|T ''gW5}^-qZ`_NuݻLjv܉>: C(uApԩmAA=-$1@Yfg}&HW00ѐuC(ot: op3@n݈.\H @UUnjpL tA&p<#4" ,@( Y>)nF#O7ܟʖ(35 pezabZ5 4NٿCy'y^Z~n7233>E 1i w ׳ڕ`Ԣi &AR~Q` oС^aZ=׿n͋3P[9fH ~PqTAcȑ#7o۷\reؗMѨVQ( _Q%Y  u3"١N<,,_|rO~$PҾ}e˖FQ C(..u{Ittue˖z+ݠu~ NPL3r5]P(RTZ(f*)TERH38ãbc} m"E uAV];txQ#GP}}"lwy'GYЫAAA۴iS]]9` ܶ5kVx/ j_وyWp ں\}cb\vQaa!-ZDDKz6=:uw794h?PߋcƼkSN&G-Y~-7#;vp80Tj룟UZzѣ=:;Z$8p`êQN3bÆ kѢjBJOuZBAV~n]&&UQ{ӦMvW~~>#Eoйnh?{dأG+'r"lrݻ[lJ 6 F qE~qv4R,ypuիWJ+T /23S 5Q+uRǔyh:+!<~!5fy衇i _^| S֚Tg$ZtRO~EDD) *Q<0h m۶;_;@&؆*пmjCoPa SYNO*سgOr媌ҥKU#ˤ$NH! @ PH<=>|\[*|ͪ9B*z5@>}TZhԨQ6Q TzHUl@y]tqwN39EލJ/#=UhEj [/@P 8qW/\ѝܻx㍍ wUO>d„ F9眣zC4mݴB}%FSZV}p?~x9/^XΊT&+E xbd/$ 7sIRf S[+A=6M7ށnS%ԚQ#$KpV-[H@Kت,}Zx ?|m"P:u>;v[hUen7;\Jio׮]ji[$m ] "(P;V4VѣhVF8_~ZmZ zXBϱEU"b!C{饗ԯa聣jhM@5 #j%' THpe-ߒ%Kh;z𪫮RDX1 TK׌3~xf,))y@ @x嗵 NZ$`> stream x+TT(T0P043W04R0663Q(JUWSH-JN-()MQ()0V50173JU5Vp`, endstream endobj 32 0 obj 85 endobj 30 0 obj << /Type /Page /Parent 3 0 R /Resources 33 0 R /Contents 31 0 R /MediaBox [0 0 792 612] >> endobj 33 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im3 34 0 R >> >> endobj 34 0 obj << /Length 35 0 R /Type /XObject /Subtype /Image /Width 586 /Height 1388 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xϫ-KvyDqF 5x4@= £% d@$, u!p7m*=A]EvzuJ=qn"{Ǎ2\c8$DFDFF|WF>@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#t:@G#7~)ߎ@G#֕~qn9w:Aꫯ:Tv=@G#PB[ t:Eqest:%:@G#p\:Wv=@G#PB[ t:Eqest:%:@G#p\:Wv=@G#PB[ t:Eqest:%:@G#p\:Wv=@G#PB[ t:Eqest:%ni|߭$tٍoӟ{a\w:+؜ݾO^xzx|ݮk#9cHm/_[3㷳۝W^@G#p'_}ūקQۋ*_?nW?t-FjV`:qq@#L}Uɝ^@G#PB`'5?yܔԢOgXG#s6gO? $vH'R{x_CFpX,۝^@G#phUɯaq8R:::#+vhLLI-tv tC`sv2VKeSGgޟt:7gZ_ȍ5e__@G`qP/7Og"ai$ƲIͅO :nݨ/t:N|m8Dn0Z:1ݔ=vPz#1mvӎvh+GМ)ުmw{:;Ar! gG^#Oݸp]:yfGT*MC6vvCs䠿1"ݜD֓tn nNҬd/9nFnN"v:@g7'iV8}+ՀݜD֓tn nNҬd7h)6c5z-!I\dz_heN2Ik#wg7'd;[B4+6Snhmg7'd;[B4+M6ݰFrS-,1ݜD֓tn nNҬd7qY:4뻹$ғt nNd7wNrv:w@g7'AWۯ} ߴDn hnI'd;[B4+ ɟ?DnGt6$lG#pKtvsn;bn YNEk#Uleݜж-|KJl$'̪~{؆ͪ6V-rGvvsѓU[bj M-ݴ1צmqg[>V}vgiGn+>QC>V}vG?y\lvӅ n+>QC>V}vckǩ8eg >,~-;nuٖ$.u xVttv[!-Hp]@+ >ߜݾO8[ϸp06rCg[>຀V}92/Y;SoVn7l\ºO7g7Ǎ=\8!n+7x} hea'[J^k3nёI}Gm(϶|.u[?ӄ]~to5¸lW|?즳J[~vn_k"pvMGOnjc[tn*o ɬMb .qtb7ld68P}* t9јa+86*MC2vC9'a7$9XS_~w/tR>߲5z-Lw~˾֊@^}vn^WvaCj/~m_C>"Onl|{/r>f:vv۰Q/ܡSn(|?O?4~Gg ^JlP/C2|vC'b7&R$gNFPLn^vjjԦnyOew~W{mWSa"iSnzOe6wcy DvtKmWSu(-Uݢ}]S^"O'a7qD&vSOc}VEZ460| M}e)';bW2nlyuv[!X'ÇpW!l7}vo|":㗎JfImW${}* %r`+Ԗ7H-ݫۆx5VAEH@dnSn(|Wa7 nҫ-CaKBggsuv OuvkW'nW eoާP xg \ꛩ~ݢXWh\]rLm? Om.Klj[k;u4&-B:>2OWhcQBKN6lW!٠>vB|>톲O7g7xmnYmRz^\и6-/Oe{=^O'+4@[ݝk6.]g檽j i]4vZ䓗и&<}* esmu'vө~cꭳۆWNE|"v#rg}*[oTnFX3f߶}''Õ/~(Vh\]rLm? Vw*a.8>v;JIiZf&p5 k,TbxQj Vw%G|^c<9=o#w_UrQ _'-/aXnPqvCA'-ez>mۆꝰͫ $ߧP( F%n,HX7P-n7Sj}ѐ-Fh}VtER+4+rGn̚<'+4zuԛ>yçK1Q巿5[٭b^Yʥξnƕ}=Sn(|X`4X^ʍ 8Cuv۰^])nCn%vC1'a7G/^RۆnW|ױl7}BTw^2ɇq0€n^])Rc$çSn(|ohadymRzug +XT>_V; ٘wݸ 4mFqū+mnWcܧP ~xm[uۆf7KSOeh` +:mFqū+mT?v{;[B9|>{aD?$BꋟgO-$Wh\tvC'+4Ju\Ic`m^Jw zvcϩn:B9IdmEGs"@jTW!+4[uvC'+4Yu:E:m^J!P;4?4\zMmh4E|6"B2޾TJ>_Ϊ;颠r1a{)zV6 rCCt솁ZO)2_ct- ? и}vC'+4Yuo gT>6lWzVAFE9+",܌"rDgaN'=bRS kCܧP4 RAgZoǥ%gc KՕJsv{xd`OC:cp62"kP,?q|nSn(|Xe7kh:\Jۆzqճ@RB7OKtSF}@q^xRڝ݊ܧ-pNKx:\L%߰v<{j҅n8%YO7cǓ+4qrvC'+4JuGRC#i C*8|m^JwVg-ΣỈoiY%8Rí%ge}Al7}BTw`7.a4n6+^])nȲFOiظݴP(K}Jȭ_,TuI+#>w}vӐ-Lxv[VɳݼݨBL1nPٰ8#0ca@Fc!) 8f+4.\{OeD`nPۆWWk)~3K1nAavn, {T>I<.7mx:m8?;j5}!|Sn(|X  K>v۰^])nCzvc(L9%r+:o.#Mb73%,O&*Fա.5 Pbb;ZRxݮ߃.g7j+4؍\&(56˳Ɉ7Eutvs\aƍ~:9%zqճIԛzMhna6,'NCQx:蠎nNHݱ&S&Ok0)Id-^$,<-?z~G@|0lbY /^1DC8qAvIb.cm[F/@}Pw ~-zJ'ӓ!KFG+L*wFkF` n1 _d`W=ˢNoy9}g7'U;D^ %4CZ!RP=EaL𫺊>/'|(.ʏ3@rS9uYٴ?`rvv=;E(uT; QṨaw[VUDa5=9ѷ 8ܖL^Jf N^Y[n>vd+EwoJ<%B pp D4PGn3ă@;$>|zOk_ޣwvsTn8t\ Id-V$SÿN*U)twp(ru5P現_Ϧͳ^8"+?p%P-Y=a6|6n0T|zC=zn;9RAj^_X[Id-V`RP%IƯf \]YT(K& ̒hQ魷|ƷJi&72"KQWLs{:fd^B5ӯQAwId.u^O¨ U-ڣ$d+mTndÀBcVG  t U&Y`+Nz܇m98ϻ侳%h]^ApA{ziS'%E'naⶅ; g7wFjZ "&ٍ2pk zO#[,zCmmݜdt?lYnN"kIRqg%gp WvkU`p[rxcb lVS`r1p Džxfs܇3.yVͳ(>㛎$7?uId-XiC)n, b%I]mUE0rn-< ?D(OZSB b%10C;9OuvsYK~a7Pͻ>\F{23&氕a#FêWIrFVC08D9/)u @B%lW RIR~ꮳZw A:ǘ!BTEsEE@?_)TJr7g7mLScW|A4&Ӊ "QBaR u6$uG k&\ .bvvsYK5NzְrNL!TnDgό>IORdxn' A!&"3vćɔ(i4xC]zQ;9IjVɢp6>I^Ί{J eMCF}61eT˰ '>zkFV 0FTY Fm ^ cc)SfL^P{nNE pH` LJꮏݜD֒-(k sV\m$Y<^vja&)VFovèfa7pƊ,ڠ=n?"|dPhg7'6CǕa34^?I.NaRzvKzD/P#SV"l.rn*;PXB/~,QoovsYK;nT悚MUDfM+o9i =R`|N, . 6SM6|nNbTwυ>Zw 8:rS`t4o9Qd+m@0bኇ⤷F;N#D7Eutvs\ R a}}7Pjn-`BN џ):ҏ)CQL}Id-OLqnG3L.UT\Xr7Թɯ*՝nr<'8%[)TЕzvrPsC2"P_쮂8oXSlxzЁ.Ÿv!aȴnGA|  UoK">g7'IU;mA]@gNrqJRܕFe7 zsٲ^"e 6s9L &)֨[nn:v$t[XźJ!ժYy[Pz;iƎ$Juc2EBmI'$[)(kngٍx V,Ü$ M?.~Fҋ(IBѺed+jSJiݸ6d7ϧ C5 >gB` ? ,$Ju~11Ja($d+=em=l& 'Շ9G "2|sZ[Fht\ր?lql~C;9ɨR}mX] #a7Gu2$Ւw \n OQ'w鲿hrnįz̦hV!M{h]9{NBd)ooZ6 MN؍bn:Iad+=b ہw0#+I#@G0qt*wsv4[ ÎkMN|vj}9*|AZD#"Gd87ԥſ*՘ -'vÁLJSR}4vc&%Zt',lK25Mv&pYEAQZUf!M`,V~p`GߣROkrmL"t&FH4::9 Ri&}cFu'c7:`d+mX)Њc @>Ըָn{f7` ȟ5%! aTܒB)>HRnuv:Ra"vSRu/5vIɤ~u p\~Zb yiv-Q/T5eZ2 io춳@+՝Vĩ(:;-.v-Q)nC=E,opbT1(u@|XC'.1SbQz;Jn;9 Rݡh)3ٷ!v-Q)nC=UT&58A~6E67cލh+7GMMR;1;U;݌Sn-r>ۮ+=ح'6=ͬU 7ٕVnSLf}:Bt]Juuqn0\eN.٧:ea'0d$@`MݛݴMa:흟3n>6JGgJhR4tW ~؍)r 6Pv0mmndWb䙉v+>iD.uv+!cwv3i TwXn0]J=[mWȕ6Z ڜݰ1yvkH=ِYO-2O(|/ >2{C~PGg7'U;uS%J6؍5xb/K9(&ۮ%*=Oq`䋔g"`hS`M;a7Uzpbqc&#A1P^wnN"Twb7}4>cʯnݘ^1j/9N]KTk)0b Ϧ{ FT:6:3v00ƅkyj[}PO},nNTwbѤ%]m[[& YTnT6 {kaESg4s%V{`7@f Ke/^fV7f IHM2삙\ƻ1vcR%ZZR܆XKA#&ʖ#C~YÀ#l*JcUMKٔPmg7']WKĞٽoKqQv1}-]K\$ekJd $&sAnh_j>N;FNYOO3#Fa0x*4e+:yvvsVcҍ 5~1'i7i\bkzqՀI`4piQZ@Q CZWK;Won\D(t!p+^8Sv"qo9?gg7'aU;|h(~eX_?-1RY*fwl0y;5QKj–Fui¯"qzWٮ%*mY6LP W:,e'ȗJHQ{C]Q;9IRiF51 uW7i1DBgCv-Q)L 09^?,b){bM{I^`R_q|yq>F.wE\M!O.0ޝݜDUN#_yY8a8C"k6Z a7{;H"@Q͈SHg4gڒh(2%ee̮GwHVSzctvsWj4ߒÿ"k6Z a 1lHu2 mx&4iφՏ蝾^Z8QX@%WV!uxCnNRTwTxacIuvW!RP kE2"^V +Kt1SJf)eonꋆpJ [u4EUqt0,/27iΏ$5?uw&zBxѱ̤3|0!&" `)eXv-'vٰUg7aA+D~x7:37vcmsB+##]꫔ޣwvs vjZFÊj̶J$ALn8Vk ?qװP|[NJmF7K[ȟ`f 30Oy?o =wg7'Q`7FYɭtpJDSEA'n4g84g `r2 ӄl8xdF@tqg 6:SCyuvs%vzmbR 2jVW~.kh#Mk ?q/n kIjD5.qXYkv|Ɓ9arQ",X1,& YA&RWti\άhxQwId~-]"ԖSI>#7$hZO܋ N6aFjZ0cCWKPLE3uon05@C1ԢjU7/O /IO?Mn) -4E#fk>y?GH_]Kۉ̑ݴe8.lL1MRp6ߞulnU吥Puf튄RW \ȩ(P:-;]܏޿M>4 =(rE#XaaK7S G& 90u9M3ٜȌ̆2 6*&+(]Gtwvs)v|h!ifk߻$2]Kۉjm©ܨV|,,=9h3x77Ŭ$-[\<i~d79W6[~ ,Dx2^eH'րv07N`'@EWpܴ\OmP'Y:-mYfeŇ[{Ek ?qװbBMLٜՑ_{6 Hw" մi$NM h pk9'#' vvs%vv2Y3'>hO0xVG"ZO5x u.+CBcuGLDPŌt`|W;1csvX ]D+4HN%3}^7Ӳ˧M۫4JЖ hr~P$۸O5I" &-I?kOY<YuxYJY[/+Gb\y?/Jj[$#?uwK&q&Բ=ao hbլj#C$u>VTʟύ։~=siSI aQZ`wiHٍ+a jJ]ʶ”15tJ1(qo|"`7VJx\nh fZ9𜮙DLc؁hTv(1Zڵ_gfF%|aP`Ōea\d,\ᜨ<2?v0sg=7vpmek! 0G6ч59v!T670ّ-rDk?޶km Xt@Y< MC3yH5, cv0cRY_TvD#2W$^ɓ-34޸6ȦtnROKԳh!ccv776I؍ zf;[#nW۲۩hިاyg8 qy擻Uz;r9JptWom%ݪfSR|{, U7ё k7wZ~E;yvzCnZgZ2%*whP9B% 2g]J v--Q.$1bעF_ M&?)یFn3Mm;|OcONbxxC=wv۽v38y}E2ANG#ʼ]Klnlࢰ9^U=[P &ܹ?E&knJ}c'*ϣ:@NFc{-]Q7ԣDv3*`HUy^E))[n U Akmٍ5!IRR'sj,+Np #tvv0y{DdjB@(\8Ѹ9 u}N>bf؍2jy漾"y(mV*]KlnXERI6Z5dԬ-oPuODfnRGRߐ2,C_f|JW$&e ȰB憏2 ^#3/~P!GRf#znnJF:.[*u$8RWҋ@ wrV8®tv۵xr%v˕s'9 %ٮ%lvC!31_n`U+b(#Gd)#,r;J)29 vSa Foˠ^xdRc,"`L uc6"4np=V4!NBkYvDƕ*ؑݘߡڼkSbʄc&#Yp)ٍAtik;wtk[6cƼs‰ų;J5_v8v)40J{ԝma7N!3Ax&b'x;m k<Ѓ_!8G;ٍPAP}yf\T?#kmMyݲ N 4 #ZhSE5)ckxk؍O`fk 4#P_ueC $T-y Θ^93"lQn2r)r6vE!svk` #sk0:gt)aklۯQd &2 6#DW_HIG]K\nU_&IA T)2p>U &YSCQk2q9I򙇍\\C ! )&f'* (BqDSC$Hp_X`deGky7p?Mg n N`ZIvz4ʄ; Mv-a"EIqgMoDqr~9 )ޒ %)B淬oӐA6 R?atLGƼHa hs 2V87}SF/ zw[꣇vv;.I3,p\ifDik Шw}xl a7ގU闡E),SB*E'e[ZY:a0Fx/{נ6bи}ۓ=E61T ahk8 5FJ#l^Vu3 ݛi.fZbf}jr٫rVfڵ, =z\HL:>?C K+_ ܝ e6cF:zKh }znz ,)h5x"5-f#)&&R|oK=gH*؍l0 Ų7:b7D2.CְV/ly m-Hvw8*d.ʽBI/jlv}4ScŠ׈B1"+HmQyIGgÉ5UJ-Yz漾bnn/x΂IF*7-&ul۲ jf9u1{6&f a_#- PYyC=n;Nd7nZk=o)mn's٫5#ɭ_CְBR9dg8W'y\W;qP_nW i> B4f963yn,/iz5w۲Z5l a7F4/_L ClMlfermv7ͯ~#Y%cxw ,yK؁l$M]Gtwv;nݲn~Lje sd!fq?g,&8uB'V6βMYRѓ2eI;K\9$fS:yvv;[e7:s*-}oIТ,C_qvfd L)8y PɋhJ>tαD,,YvHdYG%iҤٮ KyD_dN)5o+hv+Rn0`mY ტ=}1ۋXSv-a"ŦGW_n:ؠ7iYn>P(d-U*_ ,n#ۈ"004ⷃi$NPzBz `a"zM\݀Jva3d kP-2dz՞' ﮳dvSg7&il =1%S\#JԈ qv-aj4=Y8 [nd(Bb\$[.e!&p.v0 a7x ˀJv#PQg0E.= uGvI|'fay=IvT8K܊02mE&v-1n!G &V0hy*HMJP8RWz3jHKٍٙl  n^Rޣwv;b>oh0j_EӪXX9ov+m]I4RdkYv1pj؍ٙю e K/#gt:.e7[6o#NݩSa^ޣwv;b>oXZHLp 5D K8qnfh,4,D":j؍y7Lwxwz  [oV5<$1͕9z[&kD.t~?VLc0A "/q(y.75esn{N6o7n( >¡qٴ4vBOuAǑB=NhV9zgcM\ }%S6IL·Irmv(;F,-n@4V$cYvV谔Qp DGu{C]zQ;ER17n 6*(Fۯ`t,mFjT?.605MgN#D7Eutv;nݴÚ}^_8hUũ:sOA+0Hc JCh61 NߊR,/^6Z&#<9%cXnhLSk؍pYǵ{hˢoDz0A&ϓ=:!'[bh4d8tUĹ!؍%sI?:ŵq9WSU3{Cݑ:IZC^o(m{IA0OV3Wcٰad|&5rkYvCyMhrTf5NV4 1k&K*wTodCrg p)Q9nt$2IgLd;0&#y6m7v>m2nٍ&OXUvLz{b Б]KT "oc%[nZ¢VSĢZ`g`Yap LW9ϲ6V0*LK4Fvqpz"0,P[;BXg#HY݂Kԋnt 0< 7Z&K$c7(:UR,<L;&3,y>%>H''r7˧۱EnAm2|Ixkm٭T.;#A`Y :1p`5m4& !}ص:fٍ5LBC5t-Z[.<;!ǏeF8ȳ|`:o?옮n}VJS]%Y6Rk6!2T.ZT(#k,F7v=`صyĕ"SfhLܐWg >@ fs]K7)CzP-J^|Ɂ*;Z=vӑ6[CEYvC&dF-o$~j\ZO-"Y/TOBnijv84Y ,c#98#B ףQFԴrtv f7A`KfβvWqb\|6TK7&ͿbhM=$nA]1^oG mg 6؍0a}y}9=?#}! j e2%"}Rw-OuvK%f|tX8ځiyLI-g-X&_Tg &r~ 1;Sq=o35pr v>( }Z6L(pn5 jpj6n#nqIRal1nߥ$@*0bC{#8ۺv8yE# #a4Vd:޽QR)7DC$nn6!8BNfNspY5=`rv0Wϲ{xbn)Zr̲{. svLΎlPg_z nz#,LӒFHuzVv.qF]@HٍP2Pk-5f:+,Z{ii4(NGǕdОe7;oГ=~ivG0IDz{Cdnm#,oabb>3}حV> )a}i6!2+ PFv Ar`ٳ-5FN2sk@83+/RoKopb v3τ{y}Sn)ö2s9 fFK E 05 5x.-_8f{e'  <(yCm b/Qo1rafx(hz>Gv-qQg7aհ,qjO*+P4Lf Ϗ\sRy'W=.xdQeX]F(e)]Q;py ".%$nt.f#}F!SƑϳWlFϢ^v^-q0i}Rsav0V[:ENQ-,;vC˒)doc=bzC]_Kxkj] jBV^.jF`B72(JV5U#g]=&1y Fd-P|5O[ 8L}x4|>< ig@d1\FEU dBvd/ݶ{Cmjzx+FnR :oñvf[ kd6-wm:LЩWgzk\F%*} M )1c=޽ _U%<+d6Pwv~[Z :RE3Sma٘ɧF!& /iˀf(gVGmDgDf() N/ɥ>Tz\(=LkwPI!@l΢nWZ:Mgٍ yV" z-E)a۬cݰfPj݉SB.<2,%z,?ogkuϚZ](b- :ta70 䙵XZ]Su*~*7(覦1qe>k< jΑd?G覎0ފM !ng)Rp)©,a릃V' $e7>&$Ep,KF>ZniAnV]ꮩ1?ڈorxT>/MZݨa%0LW^Tu/Q ɋm@gܮ`t 25F?yE ,L;'c7El=vdQ@}K=U2EӂjVHڡ5::ΞGE-+9tVc7'+hp3`խ_)(Őqv /j1)n04B*w ed8ޮh, , })e7T  f- zO JxCm55:o1 SV%e_˵&QQ20|l_ngko]սXΨ)vx /!4-Z1`OD`!dc`J(5`b-)X`Yٍ z 5|l[z}ݔ7`+gݰg%&A҅XςoZAQSE#8} xx&7zMkxZݰy`(sMսBz$ǀD$4&"'Oɧ>zRV=`o~0LH+-%gJv0 5P `WZְFӜ3іFΗi>O2c hxkP۵RZM^xzO/;ڔ]mGoJl :.P/(%aK  ì">5$k9-V%gX4*47mX#^j즯)Qc#y~6t`ΛxF;aORlogkufkju,5Vjx]R4CkvJ?hbzVILkEU^vAQ / F{xn_fg `S&o&cTuniaV*pkdxvv솦EpYcl{\!caOy86wWF_&8yC]SӺZﮩձ02K9[*srnZRͤ.g7=NIjq냭I1񇮛]3B=N&l΅tXeLu>?E`0,d#+MH 7MO*8q [V]ꮩգ,~LZݰtѬ0LLjq 2Tub Fk 0+c#T]i$p B폴["zSG##bȖ7NqmvCSxSjfٍʈ)$\Ty<ή>> ]m$1[5f] {0ʁ ([|dïnkY %BjaG0tuiyлqFՕNIn10f/f/Pɇc?y jVv6VTšgѫɃGCk5vS³jVT](Zlb$0ugulYx{0Kj -c 杠·Mf=gn ä" >J 6lxb] XhZҳVykF=ץprnLaA3~j\SuEsӶg5w{}oUI*8*re7Lcd1kl4n R ?.:즆I,]F`Z]iuj^l=e^ JA5`nGq'Zⶩ5a\ L;ϕ=Dc1}e7DI=Ki Bqea7}|֧گkZSOy{K M.썞_˵W>ïQ{k.}iXՉͪ3Zb<“Ģ>lТL[*vcnA-̙v0FJ!sAFF%h݀zեIROMjE+4:k`xCmVّ iv&EFȧkС˵ҙqj30D :.x?g+)20;ilYt,%po\uBge+f;e8;)Z# FPo[);#s;`f(rHҥlur-%[K0 -1"58}FdrqF|d6K]Ԣ::0Qok T` Q.NzZM5з\gVx;'EYoF[ZBAEyU-6}ym%=j- PeEFE-\zx\ 0A5c7(Rph(ϞgFuqohfﺽ>lX<YVa# 14JQYv,C%t6e7" qXd{22[N>ԂP$ô"˃޼V F::XOro#+$Kߵ 5>ǪC%<^{5 x{}V04@gBq7+F+Bk`XP;vVlvu`A%nOfJ}'DCTu*! SMaPo[s|^@DN~v :tnݰ`0Fͬ*3[cözLK$7Tu7Dnk K}37)H-[#(? ZEGf+3X[G7yzg5MJW̻TY JV1C=^*CU1h +gDgYvc]+|n<;zCaf☆ڄAngDHzzt 3^trZ0`-H](u¯65 +LÆz(&A`ٶMlD#w~ukDdYvc3.POi~'{NN4JR/9ްV;bi0DLyDotOM߸ϡuvšq aIaYʸst{}Pšy9 Fttign0J3u5lv,*Ԧ۱c}@9!F~#n#CoW\mҎ6L@Y-QcDj9zlKɝ<6"RaC= 0cКv0FɅ O=gnZCvM3A @/G" PV λIda>>r{7zj.:VL QnaРrRuy=)L^)ׇ'4'] W1r{v0k툵(*_izZMٱ'Omđ{wsprn`](ivVs Z>ZBc7R*Q5fY#BMzqimh;T#X PiΒ]oYJDU "S텏|jΎHıiPХ}UFGBFQgOCIf*M.\g9AKF/heǡ˵6iQP{`7zԡ-z'"ׇ=i6@(SP;vlvZjQ kRzF+eβi-50-Ђ o7E/H2^΢,MCr 3/<גeswp-AzzjM?栩tsc>lNUu6X{kl,*'dO{FYvQ=.xVa! < mzZS4f޿|04yVՇ؅?o u>td7U%-mnzOx k %ac ,`8:k'%,,)tb8꓅RORǽCh?u0pP4NF/heǡ˵K2TA)hb̓QkF^h`1`ə5v0PhS&\v*_;]Q7{ xZY|YpK.&;;ta7j{YIQln!ZLRd=lj xt̖ճL;{6!øJݔ~V/yfQњ 2"_ z7<)is=ћf~CkvC$ia7Dlcy&M³zq 0=@㯽>lNܸ_$;igf7nZ#7:^FdieBj*fn6ku 2Or g+eZ݂88+ja7VhL=L}%81HRùH$^PtE6oq =u_i3`/a7FjfXj&4m3&=hl>=,$L>$NF/heǡ˵ZҹeSG %H>0+I=*ES>"H\-/>Ajp;#M8^p;;kMn,ƽovG 1\O@mF8Ce7-k-4@+Mo9_Zi4qёꄑX{1xO>Jùdu=WFESj|߲-M`e0()-A5zCZ=E)n\ 1T9}\>rnt͉Ԓ=#FKok؍C:U ⳔIjXңCNǁ'}+J2gN䛂O;vNٍzėyˇK` ʧPz:[,х@GЕel6\ݘ=R[N׫,;foa7n ֪'̯, KAN vwotRJ,IY8v=@N䛖Qv0^bŇG@QoLyˆj2A%'̍. ju,PPp&<3^trn{goi_@{}؃ª8ϵex;vmvŐ2Fi ]e[fMCe# ɠi 7{G#4V_< !*<#D[_FbCkMv_a=RX&fwo ~2-"y uByÆz  *RKb9_WY ޗʢ6*y^V>E z:B!5=Fon]M [X6QƙhzndAf@a$ eg.hu*`گ{2-\B(̵ᅑ~xCmV"^G\8ta7ނTW&Rf8:`/$2u8ΞoM1P )DvbKEnIe%4hF bBs` {>qf`.JĹ 9.aU >WmF>e_˵ѿ V_mmtzӠvC(mzTm.y`)%^iލbjT^Wv0mn"lGA43M/FEdQIEЗ`쌕g s1{Wl%YZh 8ԈJ6q\MEk$_ׂk&BБ~yFj{`7F#+Qdv0mnvP̏ F/} j5 #4tY"i+l1.QRFFq]u؍h+71Mk#w p: @ vk59/>A錓Ҙ'v;M໙PoZÒ~ N')Bө#rld=4\[`R1Zz[nL|^6-UãE$+æzcXov0NA?hC),8 8Ў\J$ F ˢ޿,|6=,DD㼨J+Z!׆>j.: !/?;r"_Ďt'vW5HO/ܼM9Y>jC#_0Dc/.fU$Fue%1ȧkС˵&q*/^R!qǵ#FKog.•S Ȃ׮O4Ta/7L 2wE5vi91KsZTz8.af1]g3Gg}ްVf#hсEh5exWţ\k5h "MLiR{1)Ǻ=x{}Pt$ŋ-9Ʉխ-,=w8[Po^\1\Fou|]5Mc7vÐ>L"8ݴׇ0vc*3NHl;v6g7CRlvcF餅޼V/SgE`\'`7;Sp`8zK؃`VT^a90B`v0Dpd}wE3N# X j5]v =ᰒNB=>R6# U\--_XNqy7ΩV?7ey؃hpC~eSn3laTEDƟ/KFF䋂C|DA8(E(uvm#n0VYBmreYnM4F?۵UIӆk8~MSnsfv#?S0ȮXH3Ș&j"5nC6mP$- >=Wc7vc.z˞!1aE{؃IOy߹H"`گi-KSC6EwjxCZM?1laאQhg { vp"g?{WOηj'D-Ff7p7 BF;`fU\ A{eM#]i/X.AݢX%Ke}'9'*OfƓ߉o<|3" n|2PZ= 6{S솦ˁ6\87ԋfXr-^r;} R0G{VsC zԂReU@=%ZQ(RDt  ~$Zz z{\n$Zj=1O !s7OO:gyy*]*塺q]ޤ⹏vq04T1.0p;h[> uu^E`SQb/7zt]s[L|#7zt].cBr\Zt嘃KTLSi~h} E947ԭIXkc[jG΍۬.[r8.-ƾ܁vcA͡6o_2& ҋ47-Hu > f' v[rTCe=%P9^-L1l} 4c2h1gn+Kua,=vNv%ZԼPNTLSiC1Ue1}47uzRؘ[OB22O(o2.UR)r|Z ;.uV hZ ƘtKӦO[z8|rIU5O6%cdUwnJ5#~%`;G yRnޥTuݒ qy>*Ȇizr }Q-:,f6n0&U^'xrC]W-eمSHSF-ُ28r7Nr%PW t<9=}qZ<:=5s4'r7Me< u]&u<m0%[ ǽ})h6Fe؍H A#w\K~ˍQX8zZ`7r(fuIm?J^է+Ռ#Bau?$h`?!8!DcUYj$i؍ɝVg8ܸzq;%ꁹdu!^}A`h&/]&#| q8 O6C]= D*sا2_B_NR/+wCT'k~#T! ۿrE{]dGZ< 2Iq0q}ceI_$PוjwOXv y-氀g:$C]=4U?:<αO]vCs_p,L/HR/+أbeS)XT-䤈W2vK.#塮PIE珎9sS$'c#9{U<;I䆺TyX]R2m`Sg#y;3y7+7zt]3[-6 qy&ѢJG9iX|A6$W_$Pוo!g+w{Wv=8dߞk,HUun$N塮P D*sاvc@O<\|1ΩNR/+tܘwcvFt +*jb[ b=:ނoSuRXE.>Rn9I*P4+՟}lq?FM؂BhOH?{͍8uvcZ`"<|r\Af՝ vAZ3jha1rrDΣRME3ȷ7p3/\>[{unRyVk"TMz|\ 8aL=u($h~V9>htr[P'\gbq#Hا. >a}|L=  C1ˉIv7>[Ɛ_k>^nA $Bոk7fMy=7zJYu]3r&Yw9 Y$vEXI`=Fd~ey8<=ͻq;wH|H0ɨ&4COnA ۿrB&;?V|rX5iVOs8~'5vka*\gb7/F2fbc՗$<üzEn@-f[%wAm !WPjN"ܶ Փ, dӳi:kÖ;|0<üڙ6n@gd:l@dn|`Rr'ԂT9Mrލ!osC]Wab;-cE$aiLwUq-QW2nnҝ;&#9ly9iNsعTm<㞚A,acEX<Uq\W2|*0.;8čgwKnL"˰‡ Z(o̼,u{ hKM|I<;n2yS9%A&a 0v c~WJ; €ED?c޾_v &?o{[=Rg6}gmZ/L3D> -Q-kz@p+/_e[>IR%c;&1}:lJsћf}: fӕC}=0J& A8z>벛 v?#qgCa^dRMo._@';a΁ )t^(xnB\gb7>`Pf<l8=ti"xEsL=mߙ;kdrC]WFO/}`xg %  :ęT1[mnitw]3JXiӕqy 47 $G7Ԃ/fq0|n2wq|Pו:|E<ɞ$8P#lO2|v[&t2gX>b<3D{\Kh7xs3"!uTtv7|}Fob]4&t[Zn>x"iynzgA/׎X]vG ؓWHWNA57zJYu]ӰU̸:I1XK;e<R 5n9.>EGuvr.+Ŭ=dz:yt]3<Va}+#TCu=0uspK3=+c7On+K G 5Ku].c"r\*0"=våqOca(7w\n<֕jXFsG`,~hn/Lc4mWXhSR%.Wɧ%h-V} XQ<{S)-gH~28Gcu}, +6|ʗ*Soy B80fn| ;.[ri9.udu~woXBGVX]f3OCr[k;dzF'TuNnf39ڠbߍڰXy[e7>0oŊih/7zJYu]S[I,ǔO$C]= c}ª[/G0)>*1l̻٤vmxقO: HΨ"Wq0}T}ؼۘ"ݑ&H0vȰ!1}=gUrufEGk͹\,f8qVWI.lu&(:!Y\@*cnB\'`79M^p%袛O*C]=kt/[=,yL=cͰ r:uINt f fb77j(Bk{n)WB@ 2yebSj СpW/7uaI"ݲnw2%*$.ѓ<\'`7NOE]= -5KwüBhX2̇$P 9zT0I/ 7MIrR <.6D bG0^MfSO˭RMy0^M+vunw1!d`qyNW]txg5qsXQn\Imn^}inrntl=\b 1,ynb,yY z3=ͭ*J5Am`&\b6,7z*}5ڮu&vtjp\ygSds':n=l5wi>g?>3vEqmm3S>xCkF+"bpXsVX]f? &aimrci:9ey<2 [aX{܍.Dr5Pוjw8vn+rǔI< vnְ/Z(`bvC9T8RMǍy7֯ppdLLnX'`}1M<R:T2\= jA/{ԳTE=7CgŅ}Rк̵~ӒTF/U>uNnh`Dwrɔ֪L<Y4t1"3\UZFɀo P@j2˧E ՕjLθ!/_3~ ,rQuNn~l3(qyM0-CPL8 rRO%e7ķ s9[mVPK~H3ln>!؏_9;$LV@nVyJ:iUj6)It"peA!=`9' trC#lA#Å'*٭J5ޭx_Ue2omnj F:ÃuI 6%Xz<聭&WÏ~J;Ү ڄq0b7=zUEnTv8kf *fjH=0wtSϭ'7ԺTC"ژw=f8QfhQdyqn N"T˿fFoVIw:1é7ȭ\̈́@ZAJ3_} å'wL=~c7OnuMfc1v7[TL`$NxB bFsVunR<4ZX&qa^0^nrZtRd8EZm'FI_"E_ 79x;3YƧrTj*A6ülZ0^_nrZtR bX*X M5$>1R OQdsƁ})ln!-uݒKqy@聓 k9.>E.υŘ (#9wH5Q0vqy`'\\neg΍$b]-](mRnrZtRdJmW -Jx.{X=L]2vK.(A <ҳHq0y|rCKF>,즘 չTuݒKqy6~L=9c7OnufXUisaK4 չTuݒKqy@6~L=9c7OnuHLM먼#Td-[rx˃bX,mRnrZtRr֒&mش$$ չ[g2vK.!AJ3_}Ԃq0<>^jo@"43e`Gnȑg"ʢ#88nPRwm͸x6~Snx-CN`dFGɀj2pyt].cq\t=4G@rhGxL=Mc7Onuf(I7qĭvTy>t䢇H~G;AmI܄;-8D=Ž˕LK&Bd51!s9j&$` I#9vjxe;wc sݱ'Ɇ: G m)qq~%$=Y`\r(r*ZӺ8GfzԐP(6? n,7%@9Ƚ'nLyr) ] w2vԦ Q-TEٍt W+$'^}yfe]DPZg76eލAvTt0&5AGOZ.c7f"**:A^pLAsQTm(fdW4pxۗ/]YaYحETn sW –^8Yh6&Ew<ﮇxJhW1Aqwsg\2Ϭ?>s<8C[Cюحh*'Q-`~[`fan1&![no9鋨"Pn77^8Dpp7 Q.2vGn UFKYm@)zdV ӽm.uuv`( ݰ[EZpj=2v|^DP :Qnm8Kqp(|#Ndn3qMiحZh0Q- [`7l,^}zmc!/eqoVm .u ơ6n7Hr ϤK}A.יjD; _yƘjƾJ9vc4)..B͊bY܁8C, u&vC? v="h5nލ%o Nِ !%fUҝ3Łb-*jH pSm:ѽ)QXK?E_HMV|cݺ=*<8Nn{N>v Fn_ +}&߾ Mw7,I1Jf޾T4vSح򍧋`5^G}e: 8&Ѹ$9~ Ex(,[no9i뻵\;=vc4I$䌛E$ɪc@Rmf=ݚ[]ϗz 6A5I7?nqM=5vk:ɌکsvxmpeR6*Hf3[E[Nn-NykdҙMǡo^%nx3ݸh46vk"Ɇ6ynrB#9lt@ ^ H*on'U a΃͉r@`yc7Zj!`r%.Sܧ?^9S2cMؖ@Xg}?e-Uc+V́ [ W\Y@:?cq/y& -rns1vk~J玆?ٍ}7>ܖΊw.pYrcЊ}qhnl'ϔ NGno3].Cyvկf-W ?Sڷ ,#WoGS$[;uHN`7Kߍ_Y6Ev]\dϛ_OhwrFRg0&%lhݾGnOpvQfВLfEXB 5 JΣrzW`Jv_!pͭt{{HF{ nC,cB 5 (uݔ1 eBvCncҍQG~C"̻1=ҏO"DO,Tq2vSjĤD8N] +x*0>~XaJ= =hm=ZZyon` Qn0}1Af.%szw[aOc+fքl[:8W 8[ܘsc2?U*M+?B0 0ΕѰGv:%kż<(iB[EOв-g`AkGxr&g|k6F[#1gi$f*18ƞZ?} F[?z L\hS0v `Yح&+@xUiTnuRsk܆n8 2tdYusrapqYrmYuZ֕jf0}dOo.|7J`CY8vQ66++c06՞Պ*] p8 rpLQjح:jA4[#q<-H57n K2X?Aj$Ā*]m"-ܶZ]/uZݧ`i.|w 'n\ ԕj9W Lqnp>z9u&Rm]=BW>\%?UqUADJ˜dލMi?Ka&UP.DJ[mDd p<4"sjA1m±j8Y@)X2vSj֣#ٮ {3nCh hllExF(0vדg~9y5)o[j7R□zy0O. ǖ3h׊4|xJ@E0nSza}~ zt֠B<>[aWwݸr]QUaR>>_N߼w){󻝌_/;tvnoF@hѕk;P,2>Z!򥯺'pܹ;l)SҮu]QT"-?:|37!F G{q_׍+\+>(ՈL W'7UYrm2+_@\0;4GnpGoIפmA'U ObJuBZѿ=SccUdb`s>X׍˗ ЈTGۗL}A^?v].cVoA$1;CL"?|^Zƥ vGukS a?9HM ,t lf>v].cp[W4gdph7a|R_F)0uݸẬg]F2RDY[aޏ!Jn7@x;gÅw;v].cM]= ӖR Y 1"Vbfd2"^]7E.Y])̙wV賍yҥĂu4vkPTBg~-Y -b%T K\h1$եZ`~B&!yvJUP.[= ov*@Ag=,ˈXΖ*p׍KႏZMÃ~ 1*]-$[km",|0ˈq-buYzq7 UP.;mՄrNy0y[ WNE_#fG-xapx ē۳ j21>-6s $  ,'_az0Zq9]7_ֳ>u4%pSƱUP.XDqiɵ "c\2aBBIpu-H&iՍK8vrgԮeInA ciftÔ:Tv1|K LgPU]>/9@TȞ><vF&+6v~(z`95mjs2(|\ǎu #zn!!οToEAHD5MKoda.ʳ j2v{'͸GX[Ω;ƌiw a)}$oVq@u ԭ$ڳ j2v*·{U.z(cqu !vMz2YrEJo`r)@.+pq9]7. >jMW)9{xɻ)ɖYr y-聘íXFcs  ÅCq=(oZM$Sor1΅O[ Ɗ*J>܊e4v9y7mّ0ζdlct_Y%O30pdswgVgԮe]곿QWnž21'T3I5`)7No\`2^uz鈴 ["8ήY`\rMjۺz3F 7nCa"_=SO|g5/}JjqpxXYrܯXt7!$0kݘCiq]7P߂ToB^vaj.%O+UP."6nN>fڝ G` hGT!pN߂ToA Sf&h޶p+;WHnݓg1/b׻n\zkE ؐflu m[OʕP4* h%h3R Ű[於f`:6vSj֣PCmd.P;HOG ՛%}kgԮeHlGz[菾4V$P|ç ]ו?oH"߾} ;z.j׍+%DJ.O:xwW̟p0g( j2v kEU=-3NWP 4g1e=&@|Cm:;~Ex3u㚔ⷫRt#R7?h25|gԮeǪxGmr(#'L\`vT3zms86f InĪDq)UJj>|Toœa}u[[1Yrĵ2!18M f?qDD_&;Ys@b"K災6'XW$<e7tݸ|TOumTocih4(8pVA\nz2&Ѓ;nM4 @ GKDñ[|H|/9@@0on䙯/%c c~^gԮe6>vcrݨ#٤^Ѻ}Y3-fE8q)#]CI"/H*]m.}t= 1H7a7*C:!^`12Zrͦqm*Rx>q `V t=G !8MC˱~DvLqj)p<4vWˁuKVwT8b0v+u|Bz5hM?@T;I>E9ۓg1PMq &Z*Պ8>n1,[1LJeLRE8 P7E7v !ӝT,z)I3vKcH)H=k_TҫH= Xa|2TMCBt)^VR1c$0ح ΛRYٻ븙ӗr.[1GDjxnj9T$dT8b0v+u|BzqS]Ԙ[=ګ~ܤoKd@kPMq &Z*cA䎗@px*]m.}t= y7ۘ{(!^p|x%l fMvݸ6uVvQToNw|*]m.}t= 4;i~2XbPhZ V8NQĝu.HMe@0oM P*]m ł#`uT# ~e Jٙ[{=:2`ʱq62cT$6?F1C&k Npb?[%Y2vSj#]H 4|h\MbM 2."lEwn#G<灉Y- \n15ح̬o$IG.\p T-tO\N³.pǤ[}7?c7J"xލͻ톮n%0t\vxQTz"IhQPnw_P.!2D)V!%T뢫<-n _и*]MZ"À8.kLcoR4<ڧ /Z< @GLyI`-[4ޮWL&RWex`T 3(uoO j2v k6n[7sP6jXj50cui@׍kzREtTjDݠ0.1 {mnnAy@ ~8V kY(0Zj~e@ۘ[w׍kRRꣃRjx_6嬂uZG:(ՓO~(k0iev=7 5LBo*]m,WSond bp U%@z`B-sLAj̻sBR#aJ:HĒDv=pMBo*]m,#5A7f"a@ vyFPkB1uRuGR_A O.wLoU=0f19[8LRUpPkP`lX-q-bۣT+r?:(Ց`wJȳ j2vS$#]Pe2Z/WQzmYE>\u+/~u㚔ⷺT+B(^UYr1zc.m2c( zy-6e#/,= c.q-^T+B(^#G@`gd2tVA\nsA1)a K68&=d]?XZ" )Bn]7.\|K"bzD7iǝB(UnA~汝UP.\Pz%41V~ F)vQ*Ga!)_-{`X'nO׍˗ K\V}bzS0/,g#|>-RG$*]mؗ  CBCR&GzmNKHUc ‹Xuݸ&p]Vj/;R_'yw`O#M*[UP.[ acJ]9n2FC'!Y!rg)gD*vݸr].պOz"gԮe mvb25e^ KUF)# KVsΧ׍kDԥ:$1R j|Uo1oB@n .E!'xOš֘7HQ׍kt K":gԮeOwǬa7l;z7LGr\IXN`HqԿZ=ԥz”nhO0vǬ'C?kwѮ4P*s|rm: k#>]7._ s.At/*⬂u u=S'$I8K'8Kz[x.a62;Mր!0hbB`nYר4vRnn ʜnl:ev fMA2#O’q~ܲ. T ]7PKuHbc:gԮeOwǬKd?1UB`o؇ dIX4psѳ3v Qs/RxJ,uݔHel&!k!IA!W$f!8`S uRuGTR-8u u=pnO1fd/nB6 ?C&EqI7.W|Ƶ[N.ՊЮ>2v'0]7@c|;f]m&Nun%\]q'a͖wF u/wRJaJc|u4v'cL{Й쨜݈/ sf2h'!ee02)T^q)#]Z}dOnncw̺݄) d̓I1.."a]-{?)w,.+q[.wݸv )_ԥz”nhO0vǬ2Ƒ̻Ipp>NJX;&kХ,c^Tq)#]Z}dOnncw̺p6w쾅d=yhx=@T~'8[$]y<W?'ͤ,pmu|UP.[>x ( 6hH&ݔ2ʋttqdRMR ճ j2v$GXxvsݟǗ 6اO.)Ѻ3޾Tbq)#]]}I3!VA\nHzmK2i 1)X%ɸrw f*GJ4~d{I9qz?n\\vUǂ/ଂu c#~-ƘQzCdX?h9]""8F!~?CJ`qMryHΊYre}; Kp"!OH$%dm95Ŕ٠ґڿ߲4k;'^CKXbcܑRY@Ar)5[둮XL &A >E!D;Sa7tdq@(n#B-=P%ڮR >ҥZGR j2v*"O=-:m :2CW[wO}K_ePvAWPץZGRuJ6ȴt=l&INnmsY3 >b>Xp>$Yk [UqqHu F./gg03x]mȤ K-$$&D7wn]-k qُǷw׍kR-n?K uKxʀ$WaУ",Klcچ k73q!qؓ09 0q bBhJ ƣ,Їbrve fTv?9L(6GCqav@{}@ 7J Q9 J&AD[gU1zl5iL1w*R!i IGa7"td7O叉6:n.j&sy~a]n\BnR!uTb0Xv͸}xf7`46 e,C#frD uc79 n̻a'*nd.$7o(# T, P^q)GY0vKo pgRbD ݪL>\7IMM`bNgq׸ .[*nn a<:Iw$߄8JA߾>nn\mB|nnlũ @RA[|o3N^ۖ:תb@p7b1),~gqbe/ekشЗdE $ ppb]7W `F"gjߍd1؊ȯ^X@fg-}JuJŴ cmxCw[AvNwL.YYsF&=xqϮ׎+I0vK#tRAxۑyR(ۏ|?gg-ÒJGwtݸi!"` Ϯ[*1HAvKwaBgeh9-9cEgm׍kwŃпu4v؎#8ml'9v黽y= \ 6:u0vK%]7@cTb0 /x' qcU_ N)żDG%]7@YZchqwݎT}wWٍTn٢#i%nGeLEEu s#` [*1HO mQ ǭK93`ؒ㶑eYF cTvRAxbMZ3~( oWٍtӘe=I!+12Wd-XrRAu4vK% a78(tl^e7w9]Jz1b̳n@İn?0v(F ZB߼XuUb{)n @İxm_;6< >?!%4qmB'D-]7@cTb0xʘdVp96-!WrX@| zȮ^4{cTvRAxtv#\| lCppxI1Y5j8cɥ69 ƵA_zlǡݰdT0b!xvcNJWEp`1'LhۮuϮ^4{cTvRAxbM&g77=Dl|]7}E#`vChJ ƣbc?8hN$Y0%Y|}5X׍kt nİ Hi[MNaQqA՛h3xn4܈WjL$f@ mR-ܮCxxJ&denK?2vlwpmd2= FȈիYmjc}[!tv9CusI?.912sH-TՑۛ1{bʛ5Fnv֚U(v{| %g7Z$~,'SAuJ4yv1X ]7@f :aOL:e ;0; 3ق]2H8 @s#AhQٍ6џ>T5qȭtٍ֛hF)z搳u.h1&䮘%@ n n2排r܉9H٨_&&+|rb"+Cmrz xpntRb΋q~_`72n_']7}EF &kQYn9P=g ׯg7%1_,)匃En\ex,xV,buOc2\t%7cvpsg3eUyhۮqHw׍+g fV6b -lݾ~v#. 4ʆ]? mluҋvʧneX(P܌rz0؍M#\OsnPXF/e?.eۮtϮ^S>5v+SErfՃqVgWn,X_8\d\̲AП HbZb]7c25X]K(nn9P=guvGUolb2mmqElO1,fBcA1؍xmyơbݛ"X(P,[mAPHCỤqF*}7( v[,< äO;ܲQ@S 6r!JO׍+)c>ƐF:G_ (mÈ-Ϧ7ܖQ]72c4n mx_\Nmdr ȥM ~6d,= ޷ohph )nxy:$Tt%4A/ɭ[FL^qd}FsE0Ԃ_ W:!0B ҿCzYB/!Rߝi"kR`m7t^l`41Vp'*iݞ>g<_2v'\CPBmi> wOq&H*}(7&bS,Gݎ؍P n<1k@諾qwݸw[`7Uy10|` ,96TH΄EGIoIvQ,!}O؍ӍCuvÌd\v C1́8nw׍kw}vGω; x )(&TlY)Mb 뱱. [k9> `7Z[C7 c) b ͡>cv1ndv;+'A2XNq.u/nX?""8UQWA^:!cr8arR]KHݏvCmPX?'s׍+78 _ݜh/nCF>0a'  ~WC6"kR0m7t^l"Ou NWg7Y6! _̻' 3:v dX($nI`LI 83Z;'muݸ9OuvC`` B@ZVhB[ӱM#b#$<3'h #ol|S`+.% 1_ccey:00rAF`@pi׍_78* ]n `tQRJ" ,Rj@)Mb )unXoؓ!uʊLn A4ąc(dE1ȇ҇' =kamnYߪn0 Y}wΓgY*ԉ[`7Fní}+G26,. YF7Kooc"1..%AH0vKfZ`pR9O׍+Tn2qz*s|˹9q9S&昏!>2æEr4 cv̻1 ruZ-4nPtd mFf=@spsKAqɭ[FR݄>\̌*i?5.E- B2c'$h#>̢#yrR]Kh탞nzuv㣔llLML׍+ ]R` {قpuB1wq3)WF3E#'X(м5,$Tg7JB-Fcu"?p %װo$nl?#2Ȃ5 E-;Owk ~zm2mk]7=ASp9*-Hݱp'x^;v #ߒrU]Kh u*[*$cqR\Qo}?#q3!EU |&v1D{f|kNb y:**c7d׍˗:? `޷/I%n0sbKwJkFrz0Nc7`׍˗:m672/sͫ7y9).%QM0vKf<]7._8a79J}«#ࠜbtpWE=э[" SFcqR\1 }3nȘT77dUVb4R+T'8cv>oE|:**c7d׍˗:wc]#xNkTD"g0C'{5 a6E)/EvQ,!_c`#u򥸎:k=)lvgn߼T}:`f\^뻹t^h'LsHhSRkEK0vKf<]7._8P!5d-"`\57J M*1WkeO0vKf<]7._8Z`71ct1C"\s= $/&rjbXB n LGKqG C]dڡ6MnyۨA\ spn^˛"X(>n1LKqG &ScА;fX)00t)19؍H+|r"X(--!2v#i":Fqw  `n LxNRNDn"K(`ȁ8a769q95 JKT4.f z1W^ǡo ,So^-خxO֦7&bS,![B[B0/ M!S.ɕ\Q2le@bF6H36'7ބ0_m|S`*F:x8Wʵ?/{uv#KDs<}X<)0YͅPo|SbS,!ݏvCg/؍dX88s7Fv4apOCmq[#I1)TncTH^3ck{-g4>.e~^8|0q#b [J4\+qevcUO0sz͋UiZ~o7CL \t%·n;Le7Hͯq!KҼ8ލ #q9>0M.F:m kcvz.#+8[7GtoV+% nS|^1)P#nGгwLe7 =8֊[(LzW7Й?.G[ LNN|$t%Q]ҕ"ntȆd䙒a\E)|HXBJ~d:{L n Hj?Ԗ%0 'xM.F:KQ*[*$5=Tmrw \&M3.F:Z~nGn׮i[c7?cS`>IbS,!F=5vۇ%$0v6v[%[Tu:n(B*[*$5=Tjc2CasovZ:cUČV!J-׌*[o0vˍ7v;wn-]Ŋ)ѦD)|L%S/[mE0va' Z׃Z[ <|sʍlќ*hwv1)Ѓ'1vKE1vhE'K(Q {S`³/wr67nF.S V]t%nE0v31#P*]_||noaX$M$6mn"gKaV7wl#b %t%Q]ҕ"Wd}k#բ "m ˋ'6vvZ=Im170vZnFg՛0|y7W qt o߾\ܻrSsl#b %tHa)ݚꙩneOb8s 8c8isRt%J0vK2v`+En\os%G1ddR:Y5rsG1)м}ݎcxݮ\nbq;>f{M_]"w0ĤgK(ށnWfd- S<F̎=ÛMWoxFYo?a|E>gr>LUub.%P^e :MQhTV 15|,#ƦClwdLa1]6E.qm9JN~d:{L nPwo$W▭Ǚ7Fx~ٿݘ##1İm;w1)лs1]en7o^58?,eyS໷WqOE09= ›HXB JaEA'Ư`)zxXnp'|or?a~㫿9M$#p}d{OcGbVҹhpx.bS,!^B[B0շ[1K{Go?`% PܦO}K_WwͲ6nϝ{_̸h#.N#b i +E@jH>D5<+!tѲVd7Fϟ\M敱)ׇCxo97ﶴnw AbS,Yx%zQ0 I7o}VwTdwx肃VYcLX cJn5r.Сh1)Vڽό"wZ(1#b )m7tn׿яU^+v$43oX/>k{$rbM[V[.pyf_/cǗ~@R,!]B[B0KFUW+)\nS)b #'[(cM} Ȉ>zwΒ$gذv@O('D \2G}H/э[" KGSW2X۔炎bx FX}R`M}UB@U:C $=;ṛao;`CW_})8y0)@%*c#U|IҊm*Gm*=SW,!,1yk )VbwH䐔tyvٍaIh6wk 񸏱q PW0-{2#:{xnS<_Ⱥb}DZ1(ttc-~''Qt+@%nXW0,B͟>&>]#ݺbyĪr{v8.[/Ċ5b )m7tu_lC2dnu%[K1L߾1"@p8jpKw1JunXJ$Wt%v#c}5zՕRoA,ǀGѦJzLǍaI-ڷ0AYP'd5:cb XBJaw?2v ]MeEȫ%k bů2۵)&Vt߰Y>&a\:ﺜezb XB l Tktݦ"x-9\ߦ3M*^ҝn2hv톮-|6k̍%"t"޲g›tcMRؤX)Gnb#j$]Ltm b(lYE3u8H{joFuK1WKW(Ni2!6h r2> WB)l#r߹cn׋5b )m7tu_F#c&G[]=Sb~ޙs,A)0$ JI!DDh Vk JhM]5"85qnc#{6(ne4~S`^kdHb\ ]y;|h>cNeb XB;@[}m6U# 푵0!0cIfj ɗ\*̱$~¡zk J8c1KA{H{3CJ`Y~ Pbp∹84-&Wգd%lm2#0F?ȅa1p1)b1zj$FoLDTvX6물pG&>}QknSDtck@ϧ/vHaؙ?>0k-nuÃn\b3 tR,%1vKdhbȄocԈt8MZ"YMNܐΣnjo$65.ܫbɇ20[%ab{Cu4ߦb{GGq~!KaȰ[h0bAH~d/x.Q#=54zI.5G)r)EzKM18],/Om !N]fY&ΐrWKhVn @Ū4Fa1^2\̒AŸ f*$SGbIg_=yJmHlX!5[tܦmRR0XJm)Lǟ(@%4/q!1êKFH2&6̡̌ !Kgp\ާ6KqAGXe~,Sl z.<K00Ë1t*br6bfbF+"z7Q2'¯a4v`1jdU]Da|+dХ7Jc֚HOXhdN (#*ị=7r䙬wj^Myy4^ΚljߔRHےPY|{y(v8v+1jd6#ȀÍ쮀Ud 0<ۗ0n 8-?ZKuXH$ZoNϪj֑My]iy+iT1pmElmFGU5+j^eŅA@ʷ.imMAGX/0SU @z#KX#uΑ."$\n(,ctNYG}<|a4v`Q+Q$9z̅VAsY4|tE(B4hx9t 9K!PQ:b1pS`3 $FݑIIR(0vj1",{jdQ9{ƨ]tc:w$0"bPX2HqQLW نMd n a~3ssn sm[rkK縦"_`- zn"1AC솿 к6eX-g})mtdzgNj.҇By8|hzS`^%F|Hf0׼3.O#l{XlpXBB's%lD a#Ro67|cIw; !s!̺nSB]_Ka7j d2M,q2;R,!5fXepXM\,oas%ן ENؒE{[k mnX'pǨ<}z%Ѐ$߼ΡAtݦ \GK{`1C}ԑ )1e5F,"w" TP-7/ 5:JުPҼ[b XBJaw?2v ]u5kէjJ"4#;tݦ FWͽ}L!1b駃'Cd$|X)Paqv8"2 5*8mf++$LhHGvݦB \?R,yCc#b Gp x\3GW9U /#j!t;,lV -4 bV4 ]$a1BpŒ -]֋馐ND;'N|eDfddd>Cx2#͈<,I5}'2b؃w46[mo,Fb bAtDo}u3,&hqԃ&OF&ˤ$ϼLL88pۿnfh%(mo7ߊeb[>Oˢ⋑>&D# Y`om~aw ;u9%u4(<ۺ$ ?eF&Kl}L$N k\tkcO5`?`v61`Yj|4ݛ>aʾb؃q\~Ft[&b/%&F#A~aZ:R04G .PO7tcd9#ؑ<({1( v4-nYN*6^{m ň_JL^,Fȭtsg[]&W%uZWnOF0<$Ŷ;8"68ah_Ƴ\߶V,{P;vܪR _c= 113.Q@ Zq|X](Y?Yv=HRJ|hl_"IOvLb؃λ*mb$g1t3`ݬJ)McGii|WwkdIiCe&Nz,W_\os},1)d}N?ebe$2*#Řbb HA4 r3mȏ^<5&9LLfXrU;5;\K23rOV KѭƎܓň_PW#ghAao[IJ$ڮTRLO\̔d;5~f vጿb؃r)Ӷ#ؑ/Fha2-5K1 w M|yj,RpOWtEY zYz:,Z{*o%MHƇX)⵪ʧ_dҵ$Dc )qhj<3Eg2Θ]穱Hy?Yl$1!Z\9 g*mbkeفpvXN$ 8i~b؃bD l t#Mmh! 5 r 7<7+eKSY?YѭIS&g멠6!_8DAV:~S+=BfP/Fx֩m?-Jᣍ1,rMm^,˒z WU2ծOɲO7Q̘dz#ZLvwkzނ{zrW2YҔ_PJPb\n\oFbhQ\^( /44AL5k<P@fu6S#i?Yl  w-Xoҕgzɤd RANdg_fK~1ҁTBBybGCk0a\mTN :O FLNҺ\ 4WMiN:|L5H `m&@Yo0lxR3y6 2H *Pn*S52m;Fk|1BC .Fvo9ٍͩSNi2YZJN#yk'M5>Yn3Qw5!tn ?'= Ήa^bz3v!M@My2da1bME*ܵ<bqdiKh3;AmB)qVSŕ :ǭ@ns.A=/3r 7#\mi ev]nJ'Pސ>ͲNqР8I*q4♦ڦyq்P :ƋPj\Lse3__p_iyj4VgpO6d/pа'Z̎FPieI_ 5t prF<2HD5T-`/F(kw!(#l+00qJSvE8~4Q%L jɎ#>Y^b؃xe# b 6p*q5aq*9 \a`me"i}y* ~4iTQ,{rMD&8#6>Aik`O|q34дYwG ;XQ%;wg=L^pW:::O9:%?YRCOr1g"E&2HD5T-`/FhYt htʓbLbLǝ6ƒKa\g8<bq?Y:I.dܩo9QtHW{8#櫠6Szp9#lnQ1k1*w'xk敟֌l'1S%?Y ,d2`4/>O>dJ~:' L{uz1YZ,Ѝr 1ĖhctP@$Dq]yu$,g}QtjGf#I@2H9}It-ݶ7AE)4BRjrk}_g]oWoyʏڱɲҒr钾h& Dv%mt8Y4O 4k$o~2tyo_vdu^܅$xƂd_:a kP͘b?wVh𕟈h3P`Fd6 S:d#̹F\/heɡ_L uV,{ٗDm{cL1bme5C1)̲XpGy(JbgfڡQ%YEFb{,|%Id1bț1l6;Qkn8#N͗d駺ɫɲf}F[g1SD\d RANdg_fK~1Blv=38$!KG`q;RiShturuK~Sd;~͏@O~FX)Ѩ. -o[F0Yzk#Y^+ɼ[#>;>YX#Y2êt[F'$n5s C|1bt\d`Ya3 +uruKr,9N[GʗʀI*b؃ξl鶽1n4T2yۗx'KbcMS.| -bZ#/nkZ_ZØIOɱίQ滝\ਤ:GOuWH592G8/[N9}It-ݶ7H1I W\pnuruK~tT,f INquNvZ듴GH[e%dagQJ{S;MQY'K?M^=6LzܨV)A=ȉKl鶽q KPhʓ5ka$MsuruKr,Ŝ_#YVF.艶Jd$? Dv%mt#Vw_d/Flr% %~Sɲ!@IqcI'xŠSANdg_fK~1R[y< Z%KȑE?YNpT5J{`C/]o)br";6[moh }^wb<{q'z+3i<5s8'K?M^=*ؚ N"k :'nD#d1B_[n;daE\Lak 6Gø]hdu2YT_&y;6eFtkk2-[b AmbmJ0hh~oE {'K?M^=*݈yjwdˌ:'n{)7:􋑵ݰO-, t7٨w5prطӥ~t ްtv1{ٗDm{_Za[:Bbu_ɀa;*Z%l"mx,T7ytk8b)vsE\J3Y2dqYG0Ygr !qQ%4{(<&eHc3$w1w#k\Lh2_ RAqEtݦw#l*ڬr8 (T,F@g{x?JS#M0?uLV12&L;#\נb) '/nb PGᎃG8F1Bcoyʏڱ'ːk$^ӰO`9yUIƮsVCވmt㓟~YyZ߂qڍ } C=wB,'K?M^L\ѝa' ;)A=ȉKl鶽/F3iqFNk_,ηGWnu 8C,'{8*  ~榐eb Q2YҔ_н}&K1BFN}31S!g𓥟&n;oD8#8X) '/n͋=YӁ>5HsqP]͓uf.i>MoB'.2H9}It-ݶ7:q3G#'9Ĝ qL77|1ϜuruK~S՘dy 5mݖ*~1RߍNjdL` nG Ϣd͗d9/QF/[gIfjt.-p ~1UXcbUOwk߹!FU8 IEMOuWJ7f04OcϏ~I&u^G RANdg_fK~1btc%Fj֝dln:/ :&nċvki QmqK1Ym &h~4R{?u' Su(9J|O=n&Wm-A=hT-j~16ݬVw_cplO~ЭɫG-b)vsE\J#VxJ fv>q3(z_D#$qئ6[U( {SQ(9J|dY>a S(J+q ʔ~RtK׬;j+F 6A8 _ѭ.Ԗ,w!fN˧`%?vE]~ +)F!8u53{Z]vD]Iܙ/}hMx3Τb؃:Ut"cy#5#Ə̦-]G*ژfLiw'֐,w0-O>e8)br";6[moXt1>,٬f5?g ۰L;?[Cܝ _W/R :4%E%mxHpx+ `1mL@jQJwԘp~dGC74}mÉK RANdg_fK퍛#6A6FFݍF,H@1ϻSc:'=L ?G;#g?)| Ԏ~.Kvj(Fhdý چ°TU<?;:O9:Df+iuNv,5#dnf{qHG nLLh htvxR rw"7n7d(LuNv,5#͎6>3(ŮT:jJd"FzwEyԖղv\myjQw~sro6\\$Yl{L.r 7PC1{0H`?;;8k[Hn!YnOnL`L5[n-1䬡!f}Ƕ02' PJm[^C*K&ZFZl_2H9}It-ݶ7PPq # 1r8S kH2H#\M,PPY #+t_NyL]穱Ht4 ԏ3r 7PC1Ȭ %|2_5R]8[ HŠnKަD ﭡ ~V[\x!Y֣nK4ݖὕ# ;ई%UJeE, D?mz1NV[\x!Y֣nK4ݖὛ#I2n-'I+-IzɮTRL湆dkI)G_Ws"R,{ٗDm{c 5F7m)Ir:O%`kH;iŻF RAk5m U ܼ n٦ɮTRLydS=@/6=$~Deb;6Oڼa6M(o\:Oav6O{~e--᠝ .2a+Dk+<ՎȹkHӜ}n0 }2H\M,u$=Tc'˕D_Ϭe1ݜU[ebD?W-u*yⱓJ:SqRrGtsVm-A=:9n9,hȮTTPݣ,WᑌbX)h.<)-pۏ]:Om$jxJ C78?cRnݖkc#&jC,3hf0;U2(TI-UJ*If8vLU#?u?7h *Mtsĩұѭ҉K4K>wj[JOcX) '/nc#S&m~do6,c%.6`W,{ٗDm{㱋]mƶO?v\I[VdOӋebj.[t˥da;.Fv wdԌnAf|fV-.FD،0;Y$ݚ%[M*i 2)m]n'y8v]lƨO`mRIۤDuz8v1"ՙ&Cud9yn , 6O][[]n;J;YcmRbcK߿?rEnbňIZc' Z9ڗ4߭l6[moYɜv~N,E*t5?j83}v[}8qnJ4sK#}i) Ĉu77-d:R{ٗTR͖n4Sv<+3fј݌ђ4NWT1{ٗDFF1 +ķÿyfFbZu9A؃ξ$͖747[0YMEPtPe#[5IԺޜb)M"MJtCB^U6\:[YqGgo[ǘN9}It-Y47ƨ"薪!?wot=gD8n5P :T?[b_t;k-UyA؃%)jTbT@t(b),tnAuP?^I)` neRB1{nkz<nlnyV :4%E%Ͻ%1ݖo1{P|}nZO~|Ex,bZؽۘ2:VښW@tkƙb)5TPx6)Z%NjbK-b) ?nt;Ds{+-b)Tbn1*ɏ4Qi0ɻA؃:e*eF>X-#V:HLAy|vQ<)̑w)EB*W@ۺU  67ἻV趒2[[=B!ً85ﮕ4VVfQ@t](${Q`ݵ&Jl= >ݚVQHW`ݵJl= n d/ ԼVDt[IXG^z¯H ԼV趒2[[=B!ً85ﮕ4VVfQ@t](${Q`ݵ&Jl= n d/ ԼVDt[IXGѭwEywJn+ +(_רz^B qj]+ "$֣VϻPH㤌,%x.]څdg (d) LTQp#٭UwE@.Q~-PJHDH&ȚHPt"H ) V@t˥Kcݎ~zr -CۻvUunEb2%RRv( -Q}V[t+&uDxs=D\ڊnLS ^>IM D\oAt˥dJ_g[RYt;QD\/Kt˥dJ8),(j[%R2t{ow?1wp<Ϣc$zk[.%lK7/w?yݎN#-[r)dg[2~cCD\nLS ݾWED * _t˥dJFk䓋O =n~;enLS ݒ|xϢ_."(zM[.%nIr,YO|}⯊nZe)e3)-Dmz{Eܢ[1$k%) Vt˥d-I2E2:)OU-^>Eb2%RRv( -Q}VQھsy_Nݑ-zιLSzٳ /> _($y.Y$:/.?|䋏~wITL-Ԣ[.%@;w>fW^:QHzۿWrs3&q?iN^tdVCr)/R2t$XZ%,Zrރ($yރff>]nN}t`ւk?ؿ;}#E:S^-[.%lN7")XQDZXwwxd`[9xcE[.E\J&ٜn?~?_U"jX񞭊}Ͻ~{`Wgq ٶ>oOܢۭ/U* z-[.%lN7\^^0NX<{¦?֗ǘb;jXH85xjLjZ&;k nދK$;ӭifu)Ƣ#SS/wk?8v\}㭷 ogo1KerK$;Ӎ24AaJȜZagpXpyA^-nbz nr)dAs'PyFbQyq/2]rsz8I}4¿;Dt-mKw-Ivj`߼'h?ލb=<㄁qВ hBbog4q%u[.E\J&ٜnTـ4``,l mtwt!$N ?' ۠:YX-[.%lN7@WSkS[3cF|ѣGiBқf3ͬ1('mr/R2t{%\Ls Ea=DBBk}W~//ȮAmPs,Kp-Iv6e4Sh$gFh8./ X9)آ#.S@t%K$;5Ѝz>~({W_}뿁Q.g\戙km]{זnTqD\nLS ݚ,c-E%3*!co&t牢#.S@t%K$;U-b+Hx<#n>(~J'*u<*v5|6S GcbAtȨ( ]t˥dh #m،_Y+;F`]Ȑ_9t (7=zwZ3&ւ?(݂rlK|-Iv6M=l[ "3pm l?]ZSyC1 ;aiJ6&ΗT@t˥K$;FJ|l+Tgn"HH$CO;un !at*td[Um_g-&dž nr)dgsZJ~6ꎗ[{9X6Xw !y0ԺҜ0Tuҋƽ1KerK$;ӭ2J&sZ# OK#zMhsغ^p7s,9JR1D\RnL9-i+N7WgG`x ŻN'97^ۧ1t2޾~MtBri/R2Άt_1q;tOF JKRq<8KҍԪ1!:2dޮ~D,.Đs3D\ҋnL PCgJd`jIԏ>MEFKL˔48k[g wejl=?<(8:tcҜUi>yfGt zʱ[.E\J&UJxC' b9($ySb"19fb|RwOi;:tk_ nL)Y-r)d'nWӘy*\K6Άn sF_1'׀)ѭ'NlKt-Iv&c3^c'Cm}㎧[eΩD!ɳcg=@FvŠ[շ=L|m_r*_C\qD\nL3I7[٘HjXn Me~pB<"Ĉ5k4v,0#szK6&o+Lb\~Rߡ[_)Ks-Iv|RnpFC3\n B #sޘ*jSՌWflH>` K}pPg+ \t˥dno؊b.cȖɰ MB1#y"E}IK3#LJkmzc߳-̔/pFt Rȱ[.E\J&66:HګMm?Vqٱ3xLƌ 15 p}`ADCtE' + \t˥dn+1Z9:Dk|ZƏBgT*M3-T88,9D1et[.E\J&֦UM#%:'_ PdFiR@D=`SE?MW-OL/[N[.}E\J&IA7mYO7Vј}BzGcbZ&gL#vn ca6ΗT@t˥K$;t3|{!Zk<`-ED!sCB !&}hI:|啯K`3?2$N\.nc|ID\jnL3I7F5sӀvz8#fC_0Z3ӦA.r8fܖq?cD1et[.E\J&Ki6P #;,tk8 NƇd̂s^tsѥb nr)dǧWXi Q3}uDҍrH{  rp$=?z`XlO5R1U<@-A1 yG) Zt˥dnMݭG>M Qgt#"SPc|hl fL[:jȐ8.nc|ID\jnL Z،hm`u70Gs_ht#Ԫl$:-?ϬF+S/^0ˏ;6A[O@-[.%L-fv7;*pH[?6ƣ~̸f}yggϖjG8;PW~uf+k~3761/ɒ nVvΥdx~1i;a.;japmUCۉVsVwo-C)>!lkoAIFх nr)d'n p>™f/mo1qP!9}wiqA˴"iz8҇戣KrI-R2$ݠkw :6 ڷikfIh`ߚʡ?C'ű{NO  x焄KAX9R@t˥s.%LҭikuVD'7%BTt>YS @3y]W%~/~p6OjpIG) Zt˥dnVSBٚN*7WtC A?fƦmj10pUtH ( Yt˥dnԿ mA p[Ϸ n֮dzu Voo N(gtQ#l}Xϑ[P[.yE\J&FSSml۶/ٙt5g̳]5#ƬI&L NrTe[ nTWvΥdnVwNJm%tof$ "T8 c-B˦C>E/?abbrL#dǶ nr)dǧ/Z $!)XqONh6FɣcIgTkjH`)ъGM9i=:N6ΗT@t˥K$;>(m4k uht>GyĈ"1}pU!Z)\L^[;p #i58hqH4~%mHE+Kq-Iv&v{t sm5I7& ܃ v>N`o=bNE/)$A^"zfՅV'Vq4NB{p"uMr.R2N ݬM2Lvkm$9Ơ0ip?hB5y Iɐ@7*.3*~-H!dž nr)dǧ/ϨY=n { t3,ǛemUpoiô**uS%6*.T@t%veSv&Ua5n<`cUI4<)EÕȐ`ҵL 8xEt[.E\J&F ;ޮӢ% 6:|ʘCx$gЦ"Vc+/'V|s7 LAy'آ#.S@t%K$;>VѣG퓎;nC :W' `Cgl4xc MftijvJoSD\nLӍbV|9zڕ;v¿# 6:4btb w)Z2Mmm7?`>u7C$rnmV-׫r)dǧW)CHZ:x-8p '2uZ4Ǖ,岐8ʹv񞭢1dd0gZnm5J-[.%t=Bn㎧=CD1~Ь91wrᰁ80wիɽ؂gC:{ʘ}Fۘp^t Rȱ[.E\J&le#ט5OS/i?ye4'4W_k;6[K497S@t%K$;>x)f}mA%ףg GY̲?r%ګ"ჷ5t׹z`JE|oI+ m= }UB7Tq9hy5kf|7X3YNø%FaC1^'Qt "˱[.E\J&F=Jx;t ,6pl ~V(bu3ai܌ }pY>qO6ΗT@t˥K$;>݀}mXN7[8T}pd4<|MT5ͤേ 's:L6zt[.E\J&KI,RDF?R!4f"؉7n1=rN6Nu[.E\J&U}.%y>LaJZ"KIQt\' + \t˥dn5V~70zߖӍZ OјgR}f9m`Kְlıiԍ[LuHUmUD\nĹ~7 -9g %Elfװ?5Ez}cCD\nLӍbn޷a\<ݚŲZ!C?q-0L5MjdIX[_)Ks-Iv|qu-B7.m{g{ y暅%+Mj09tMxҥϟ0E6.9R1D\RnL77 ll񹴐n6;Yτ0w G3wWiƬ3 n:zǒt&tbPl&V0. núlYD\znLC7f@sFa B16Lžl# ӊOH8(u&1nMlQۋ'I1϶[dMRpncJ|ID\jnLC7kPѣGd sġ9hܳugw 7fy6mthH9}⋦ ~5roKy-Ivbfu1ݘ6V#QA%Aa?9?mcPeóMM61#yp1HW M6S@t˥K$;5ЍF&X!)͉Qz``ͮK&5PZO#3A8 4rX{_tsѥb nr)dǧ/ҘH(j7EjlDun: rT_);=p&n!r#ƴgVoިd zySX2Ic3o;2lCg$I7\-6S@t˥K$;t$W8 qlI8t%32} @3vCŊC*|N!+!LO)c`ndaD\nLntb W%iOg6AelAax;# I Bݶ4^InQ43A8`nmJ-[.%,ےC%)ک fg; `]ښ./a\R|6+yoi8{n984=GN7~jE* _t˥d FQOEU@_3XUd7yIE2% F\Hƫ`[EzYBѭ+W뵈nLn6.tGg*2TN8U0eJ\oQfl9~t+/v {+D\ʋnLn$10D]ME]9mQ7<};DxD=fJu.n}Mt[.E\J&٩n/6#LXH?q0Ituv&|ĒrI+R2N$hU&+nSTBkd2 Ԛ56#8-EҟJ%BB47L _<}VCr)/R2ɎO7^ Sڎ6:xa@iפ(d1m6ɤ@nIrJ nr)dǧWSŪ5n6ߖD&ZwF$Iϩ!FMtѳ6H[P[.yE\J&KaakNMw˧Xe滅?7$6 ܝuA䌇" ʢr .R2ɎOWzrэҬ׬yV^kka%E!gnꀭmH:ڰdCa;v6VRt2 ntr)dgnW+QYwGvUfƍI8sfXKoGg/ XluX`̨Myݦ nTr)dǧf m Ȧr:C\jL$ `(}nbDe*+T..?E]R|=C(9Z; h 6k nr)dǧ썷n| P HZPմHuc)JUIcFЄGs7V ſ&\O-[.%tT`Vph(Ù#nsJunM &\iX?)PY#$nZF-΢[.%tc{Q\|zo$ѭfTJnN>ssi&čL&-V)[S-[.%tmHaZu_DNvsձٱӿ$5љ n4r)dǧ F1|e>m5#q~vߧC8K2ZɬtrlZX;b&yv/n}Mt[.E\J&Nt#5àJb:5?ƂRWI)*&k( RUt˥dI)PBujDGtcT teB^ VJƳn;n|\+Dynݢdr ,R2N @ 3gIF0I޹k%eLݍ>>:<;vD&:S^-[.%ЍE7S8:uƙIn!^$WSc-<h!12a!eۤDP@-Ȣ[.%tP!l w$xu.tc64D$ !ξ3cwP6HҢ> 11#\ecnN-EHmUD\nL*Z,yt3MeQ;q@$<zl I g/ q ӎ==;I%$t|~R5r*R2ɎOfSARFgKZ&Vc0mo= DY~|ծ](ӮjB#f XczrL҆曏i$ cD"Ur+R2ɎO7އFHۤb޷c6N͚IH 0hgɫ<Sۨ%Rl69MO$Q[/nзmr.R2$h{4ݢ۽>ڀ5n-Q0ރbn[ʦ=g[9 n8ID$ryML}[Ltqtu̸PwP-6OmnD&Ѝ~1[Yn!Zxe)4[+R2N U`fJ\RL1۪-2nt]yr)R2N ݨ9uKJ"Lˊ\7ѭ7rrw-Iv|1j 쏖3ch|v/Fn01z?]xvH-+r)dǧ[{ULƄN7X Yb[ʏV8n^K$;Qt du7؞o[M[R|wYtk:| E\Xt˥dnxl)I7jÂ֎9 $Pgѭ7rrw-Iv|xM덪S4f)}4NOt;+eD\Mt˥dI2;ng1 mjsI`q\^ɠ]vx-׫r)dgn PB}mx9p1tK2m9pXؒ Ϣ.^)zŢ[.%t_6/ <[ݺ܅5 mjgXI<*I"zR5r*R2ɎOo|UgbabSw؏Ԇ/Rq]l{iIE$y%D\ŠnLӍ5jL ݰԆXu zZ%T.O.ݬT{a\L3l}-zyݎN#-[r)dǧWo5H'Ot9ku ۻ9g-;r)dǧ[g8?t#1ARD$y%D\ŠnL x1: )a[ɮIMMӦyݎN#-[r)dgn!% NMT¥xL}p3gWe-Kr)d'nfofI٠#nbF@RYt;KD\oJt˥dn@氕'&\1t_1 uvNrRYt;;cD\oMt˥dn\]u$L6zvx(ϳvwnޚK$;>ݚ1Iv.QZ滱$<$"r$Ϣۑ~"zw[.%t3xGi⓭mM[%txObӍifä(ϳvwnޚK$;5Ѝ#3&ExE=HtD\J&٩nϟ4nIiXWR@t%K$;5틏~:פ(ϳvwnޚK$;5ЍE&mKv钢p<Ϣc$zk[.%lNg.$7Lf"),4:['R2tpwc#'3MtKJ[.aE\J&ٜnJ;?kRYt;;cD\oMt˥d8b\w2yݎN#-[r)d`..?t,uۙ'J/z[.%TB7va'[I?g퐯uwr2-Ivj{,66#LrϢa^#"z}[.%@7R`_0ߍ.).,U:"['R2N%t{{l6 `jnʤƳvW눈n^K$;5 \ p&b\l*G$t#s%0Eü]GDtD\J&٩n_Wy9 ݒҰ#zo[.%lN7[~j߳WRYt;;cD\oMt˥dFIl7z;e@vz}qr-Iv6e~k[[ R@t˥K$;5-)Yt;\E\Ht˥d-I2E2:)OU-^>Eb2%RRv( -Q}V[t+&uDxs=D\ڊnL#%UƳVFg=W@t*kѧQ\D\JD%jsnŤ|K[-IvD$x謧 n>WEx22˔KIYD[[bnѭⵒri+R2Ɏ$WϢ[_'UF[F1sr));Kݖ׾WtkQ-:A[V[.mE\J&ݒ*Yt++ _St(f.S[.%egnm5EbR?HtJ>S@t˥K$;[\eq=&UHz,+ Tt˥7`+R nޯKyo[V@t~E\Jγ{啯{WZ)pxD\Xt˥ޡ+R nޯK]ٹ7_5+R n^K١'o; *nܑf" m|_+R nޯKɽء }2/l/W8rb-{o }tAM|S8n܋:ڬ A:Jקp^-+r) ;T jn8hķ]>re-nϿI|9?) U@t˥K=i1`+t{(n܅2j9O 6g/^" 8[+Rr_vAN*PU۪ ZUjUR@yʛ['.T0.] Ivu&q*抦Q)ż2JrPU(0R`[ʛ۶AO@U9hFu8UM*XgEM *PUR_o@U9hs5)PUMѭD*PU IsP)C;XAVёK*onK^Dr& RNʛ[Dr*9%)pn T7EsK~oU9z* R UM-kb TZ+PUO0 aGrP'l*YvIqq*TC[ @UySt[ TVK})PUxZ*H)*onȱA{M*on޸WrPިɚصUMmi<_U:WXKAʛ;ɚ*,& +PU }=nUё)pʛaDt>+R`R66AH *on=zUyQ]R T7ECcGtl;)@UyStKzw\UQ@% T7EJR@U9(>)@UySt;|z;^-7!@= ?{啯_WC#$; R@ ԩ|W,$̮B"@PQn2.( m"** *K) & nȮJ)  n+R@ H)&R@ H)۪ʸR@ l趉zR@ 趪2.( m"** *K) & nȮJ)  n+R@ H)&R@ H)۪ʸR@ l趉zR@ 趪2.( m"** *K) & nȮJ)  n+R@ H)&R@ H)۪ʸR@ l趉zR@ 趪2.( m"** *K) & nȮJ)  no)RX2.R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ H) R@ Sl endstream endobj 35 0 obj 102355 endobj 37 0 obj << /Length 38 0 R /Filter /FlateDecode >> stream x=M 0[4$n# TQhǤ31 1ir-e|3ʗ64pK\y؆)gYFw֗~{C|q ̊ + endstream endobj 38 0 obj 120 endobj 36 0 obj << /Type /Page /Parent 3 0 R /Resources 39 0 R /Contents 37 0 R /MediaBox [0 0 612 792] >> endobj 39 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F1.0 8 0 R >> >> endobj 41 0 obj << /Length 42 0 R /Filter /FlateDecode >> stream x;O03Cj;m)D)l t1 y9GHιΰCZ+a+B h( Yߚ1ڌVw!cヱmV-=xK ?yX* 9u *R!J~tWh"[Xos}'/Ϝ?iMEıs ")H ݹ>׽y9 #IK23[Flm ZARa>IHImiVdQh 856TLmPX endstream endobj 42 0 obj 277 endobj 40 0 obj << /Type /Page /Parent 3 0 R /Resources 43 0 R /Contents 41 0 R /MediaBox [0 0 612 792] >> endobj 43 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 46 0 obj << /Length 47 0 R /Filter /FlateDecode >> stream xr@E|]ƪªULqQ.|R*JXP3@ݷ0=0- b4_HhfUl^VCa)ԅif} V߀Ci|wbxl $"ao|v~)Onȃ^V5D'JA|Xݽk]u2"rJHeHt Y+Mu#׶g$%Nx)9K{BXjBHPM%\$iKYWN~2gS8{v`iRfDQ W jѷ $@/$ f\C?fnb$o }6)0=PYzۃQzFV5IX+U)a~f@4)_4oPa> endobj 48 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 51 0 obj << /Length 52 0 R /Filter /FlateDecode >> stream xTn@+2Hc"QpRE&IpmM|bw1c%ff {w9vħOl 6ba^',_YHX+XS\NGX+dŬ['=vς5-'\bO~Ҁ'[8ysOm}0SDUZ8X-gV:"!x@ǹ}oh*Յ@ךͫ+: M2nj;ΔT6 "t2TT04]idJMnf-nLVys 4 [+Y:Qt hnZ!zG !\⏗du=YQnjy [Df9=N,E(ap<4HealWdt>҄bʚ;a( 9K \$ڻ}ʆ$)zi^VB*`B/(M)~Ik {ؕK `ӔC(v]A߃ǘ}v1Leq;-ipFw]K_8#C=/I_4fk:mӫ!ϷU_ endstream endobj 52 0 obj 546 endobj 49 0 obj << /Type /Page /Parent 50 0 R /Resources 53 0 R /Contents 51 0 R /MediaBox [0 0 612 792] >> endobj 53 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 55 0 obj << /Length 56 0 R /Filter /FlateDecode >> stream xYNH}WcƉعĄA a aVhY!/_f'~Vm\$NSUTu ~T S Byo)YS}UI_jh5*}UCP`B]X?Clx<%Q`ZvX+-ٺ U-^"J@|xf"Nm% f|l𝁗7]<)Eٹ+? -2]d":-…bfA >KGo['&CNח.3^ɖľIt,ͼYNؽ Zah#4ԺD@#*6 Ukh+EyA1j`$wVWOT)+ewI72,;p~7#IqL8bŁ~=X1F/vOPV$tT!+*gE2}AK~|H8 f1lrLYn/&/$WW\<|buRK1 ~˴D0Ckc't!œ0"jBښQD"I}Ko9ubԐ=A}vJMCQ z~٩ @,7RQJblg5e#JtpoJ96VM qM^HT5C~E8JI=3r\.~05VF#)J*liNMkTr9[_qSG0w)WtE[%9b~Z?|)}g.C)5X6҄Ggrd-"N)Ӂka DhnA2,\X&slb*3VbaYs8U}PM> a>`{)(kRk> endobj 57 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 59 0 obj << /Length 60 0 R /Filter /FlateDecode >> stream xeα0G8Z]58FD8|)Mpi^PaO!J06Gv` K$>"U2qPwZ/qJͭHMhIas~]ۚa"=[BG^ QK~w;q9{ endstream endobj 60 0 obj 161 endobj 58 0 obj << /Type /Page /Parent 50 0 R /Resources 61 0 R /Contents 59 0 R /MediaBox [0 0 612 792] >> endobj 61 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 63 0 obj << /Length 64 0 R /Filter /FlateDecode >> stream xVQo0~W[Rl LNui%l`Yԑ swߝ6 ?Q) & A+X$VYȖTO>Fly7 ޻' "9LiN$pEvӿA& _i:x\QXơ# ;݈Q`yGy pN{ G<vі bCI]i7G,A?Z)2'x[;) <=C lE)I(]xs. &3>d:ٓ@~̓#*g3Lx˫jA\^[W"QE GPmuBƗDrh>CeI$$Z)ijbh(}7 տ>ΞG~j7JԽs),hWgY) XLԄO.}8y0lVN*՚oZ:}zHkP';l CnU4W5Rݵ K4OKI^9;*V^5a&-iBe*LzE.R}_|qMMq&l8"Ͱ835"=Tϔɷded1_/ָi NXpdppn endstream endobj 64 0 obj 643 endobj 62 0 obj << /Type /Page /Parent 50 0 R /Resources 65 0 R /Contents 63 0 R /MediaBox [0 0 612 792] >> endobj 65 0 obj << /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> /XObject << /Im4 66 0 R >> >> endobj 66 0 obj << /Length 67 0 R /Type /XObject /Subtype /Image /Width 570 /Height 358 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xaFz&c}۟}G7N<ӎ:2D^qvSbrCI45X8i{Dd%Yn0I3iNOsL8w AK/@8\@᭷T*7F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`y888>o#)p:F ܼykF`j)F`^V&t#X'ֱ\*0ۀm˛]FÛ}tD[:ׄ#u8wgA[<(q8#u8w}cVU!v ÖMaޏoS$U1hq/ۜ1>',lv-fwF5)J`Iԍ>*DZ*1x=nIQ hiڕaӿekƌښv䂲dmɖ~t,SmTF%]Эa%cNte= hE(OQL j#k t*wؗ?2ETba4Z!WSldu,2}+sZwhALk䳺b(WX^VK7DQ&r(Oet^Y3+l}XP0 iM`֍p`jyg=[}3Dcv醋γ( o{iY7t'Db;v#- ա_Uc'pRlLy;B:MhtGָn j7SA I+ HZxs QƛB?4nO%d9KonV Lm y"a7;YL/y8hd3ޡ]Ņ1ɗBT;ݬ*ؘ#qQ7S̛9P*K*r/K|f6eY}gE) бnsoތ8%Mwl㛪nii̻g,1~O[@Lf (xf+УC3YDƣ'AC.EIitel NK`|X Kaъ ]+ҚpF =A+pV>ȋp鎔 )gY*KB{5JMh`VY Q,`ZKJ҅R 3]e o@ uߚbM f PeуlV~JrY:Leuۦ5D_8¬ZMy;rHF"`1ewI3x')Eg#j,uiÖcrR6ZYn^ NO.KŊɋŷѠ& `0KC/tg&[*Oi]'Vuۮ *x]Q)%z:-- 8 RX[j hisP`>~-z5\Ti;?(|ATkbe_A)M. U*\8Q2uUCtLsCbJ!.hVCl oE-5ǟEhl$i)Ȉ֧-p7~Tv6sĚƙ=)+9vۋзt +ja54tUkwNjjT:ɂN ѱ ;5\Vkت,lWu!SpE&/+rWb}zŮwVaBV*Uz{dA>JvYP)o4l&ɕE)S> 84pEަ8\5#~cڽv9C~b2[o,`+!@rc| Mdi,d7l 7VnmA6j o=x.!-}wBlk-rf)@u8HPCSPc!C*]"G={f,Bn H[z ʾl,Ka/EC ׏=qF`ɖ碱o[u8!j-.2 "%A],bFI' 4_QWa7qhSD|-0CС+wb Fo@,^`QB[Y['eVEd .B^LSIdXir}&B+7>On,{E,_E v=sc'V1Dh&QvU1-Ht$NAF8V}Bz1]K`~ $Y@qClY)8# Qu!59)NzRBmxf ئaZob n޷FB/ X\0RGωgʕճǣE"WRiXrߠ뷅tF5;RhMl12uI‰z`;nCQ[(]fn#S&BbXMgdd![_[ UHb@y?oJݻM0~Es +bJ~oI.yA-b\ZMBHtـF)Nc6AMWj gD J c:GWO@9rrXF#.}w{ΨI Z t=r}vC\y;qE -}Apz.Ղmrx+= " @ 1 ePj16=u.#zҙV_l1u8x Ėlu4D y2KQ/)vjca]uٻX/J3[ƍ1| tYAL9y29 mPԞ"k҅9t[UVڑ` lk 2C^LF-Sn{w4긋e{#3ojjV](Bo!^Zht)viHAe; wFHETiGF%]>H/nӖf_ ʉŦҍC 4Pp2oZnt]PfB] Yvύ@`~֏ys\A{FC~J4q,ЩL*>_M,}MFBhcFJC:*5 4JJѳՇ"Sj[-hfrԚ<#AHX*Sn hfox.&+TWE x j=!wl5"QZ]GΆ3RAۮn6gC (b0Pḿ?7lvc It6KZYOxa+ܜ|fu("@$(*mqcz4)y_Dsyr#A6Y*|leJ xA1_(DJ᛹Ű!52;ՕnD [=´"% (%-@V-Q6lvcu&iZ)d0GhBٍ(mAZ}5[nB1){%_TjYSm˷cl~ G&J.6FBrr]B\PIQ!v, Rfa4m&t'Öw$+YMŚ15N[XȺè-* 0Lڐn$ò܁)U97>Wi5pmrw_eAOp/dYb7lvc\A *4zLNʤ#F~H`9y  x͓ߒ+ĕVSsV\USrer܊lV׉:ю^kö)Tʾc:r7<=G WRJcXfcBqicl`#XkK_bV+rC̬zahTug(v|DWjp+IÛ=XIlVI)u8 @HbKgW:noX3ΟHIlVɫgQz:<7b60)o$3[-p ieCl^[N7ycD -jΛ%"A|=h8K|#^e*}[H\&׬AZ$H0׊B{ygG,UD&uؓ PSk 5]= #ʹqg8UӋqO\{+-9SwcY"X/p^GDd 0? "vB>/!,;:<" ,җQ 0[= j{u8HPC-N!t=2vϲF+R2P( $B8 xiRLxs'1NbH.)8TZ m/bIMxeWLv 7ְשBDE"ErU X-/KvK2Jo뇍j9`+LORPB}C{iܮbX1] bS9 "TKTE\sX \V{l3]Q^LP֙[ח(&İ||bZ FiOeӉd6&/A .ݍgg.V U*QU>3*Ju8Vy@+|bsXQK_+@Vk;Ţۦ.uOi2B,;cOSxlVZR JrP1.Ӝ;wvX9}u1[H'!:lw;J"A+߲XhFMV 9;6+>bxmz$M'\>F`$3[$bpfA -xJ\sMb+#_ҙ[ó$q.sۊEBV mHIlVj9:-=c9&FP g DYdӺ62X*eXY==.vݘ5WVhsyk OZ!'5Qp]EŠ!p#Z:fޙ+&ݩT&v!ElR%eF FחX#Q)u 9Ů/bF|hCkq-"U\F]A"eTj~" alrvr8FZ:U1ȝE@T!tB}#iTIG%;ʖsb~잲SlpDd-Ь+a՛ DL޷,`+[xh-JjOd"AReL&BjJGY1$>([z lfB<3wb8E oENU6(}tZp8DrbL R,CԸME ǴV11VXot|VDog]s%/$[鯞Ml9͏??rhNl;cZCwYDU{;`NX;;Opg& [r\luKW^{]]~ׯE.JیVVwC0[ŬS9DrT孻7tH<=(I9[[davM_b ܹ@c0̧֏Fڑ5 l~#<34:/q_Džܳ^zĩS3WAN>ub0ubӧO9uw"ɓy[-d3|v0[;,"Ej͝ݯM܏Z@wq=D'wy1X{s7n߽oۏlsu liG׀U<;>]-?{^ȟ93ݗ~Q䲇n\ۻ~,n]ع?S_=!bb]"l5?VGa0[E0gGjy B`Jdfm\p᳌@`]`fşsgAj8f#lK0[m}R,2z`ZΜK`aFي#è`a,#l;oVp0[g8 l>/VNj?̃<(qFj/n3`ڌȥXfek l98#lþ1f+ F 0[Q#l5lXf1d IG*w.s` LJ2q@*w}8^ΝfyP4f_.f lK Vˠ2Aj=8s.qF*w}c$V\f+@`=bF*j>#<VcU 0[m]2#leUp0[/;#0Vi6f;\@j3#bAeփzp\UH&0V\#l{FU}F`yǐ-$fA`چe G*>f8x`:^9wF`Al6V}tfG.20[-_f̹f80[qM`0G*=ff0[-![H:VI6 l wU8>|Vq "lus<0[̓l6r6f͸\e`Z=7;?o= `˚-31D*7]b԰ӟ՟p|;w#U0[m& +rlo/T~-Yao`ZCD!~<4G#ebUlo ;_ F #m0[m& b~C9_}v-YE`o l#wVoT,,#g|w7F/>W)71&v"lK ݿ|9fɦ(/^5(eIA*)wf؊_:eg:f;FUtزeF`گ*fB܍P9+7${0Jptdhwt;r,\t^#븐sFl;(NfE9p(䩂$ʹce!\;2[mMHh[Nus0, +TֱaTz VPEnj_j} ]FV]/d>(- ,-Inhl-"T&6*TVYlKK&y[F1*6Cah LݳѨU:. U ΅, xXE?7)WfMQeem621XZ:)747SAed鶛[e()jj6Vnwp.[v FۘkdeVf-]}>eBSɤ+ PoGSv]HWTy-{Y1  oNVs//ڬ`cmrD5[a#؊&2b/ 9J_aZ.k]_h1*4VٽZiݪ`-oQQ ;GUA.َ\˦rHezit1(Sٚ'3%1kn^O-%iiLN? ˩Jk MAS,A`e5xߐ\6F+k2h-XmʕθXu'8uN7}CY@fԯr\Ü3h(j#êohUiزzMrV*U%O&VY &[I h˵O8hL_}4p?[RY%`UCB:U"2%N^[^:ohU XĔOcK.SbW[i( Oh!*XGepw,7˅[o߸M+-gu͎LZN[zznO[s[V]cviZ'b+\V5UEM=2NVPRиg %oY.w7䃄]#h1u\0Ꙇa-go. @YV7ZUF ܿ=cw_m#"\6;Njw{k˹[](jKu6{F(Pf?&H?|9/dۉ\j[s/KXk_K{mE?bg6mFbIRIq _?ߟ?_酳__ķ%6拂+xaU5/ ߚ?F3/;YV.jXUo)d敦M| 26fGE%3"ҍѬa *m&zK< 4 D)JX O6ZU"*ʯkef7 UىPA(Cӯ,b)(xZ]K-haᇆr- p|@N*].CF"ׅ{*'3tAd@T%,D[ʷ($|_TG]rV" iJœD٤=oYVQ͋)/^5+%>|p li]"TElK/4 lk4^}$"v")I]|N {V)(Mze &mX^Vʋj `RGCs `d9(Mht bzȢuϢ!pV ATRՊd(x&VKLU-|+&^W~_ tU j |n]c(k6z%I4f] surֽHiq>=m Rj@t ͑٧aR)O(CpF('%o OXĹ^r1,f* k ᒠĔ$˶},] Hʋ5HDKA 4I$Ri=9 isVu05iVML|ėEX"J% ]Gcd:Q9].!"˹R+ 5p ]岉k^S3hPalHl5x_ |3T@ۄ~GNwT[4*"]x=Κ\fJf:4(:yg!߁ F S%euJ^мYVWkJ .m(#=xwfK w8p,[a&ܝ)e @##R0N8iddX!2 6Y懠Tm>ϗpSTm86;] k!ϋqè7^ۀFH1b34* f/0Y^P}5i* h5Ev\yt+ebW t"jojYɀGd ȺD0~#jVtzІ* ғKSٜ0Ž2$n"xו7F`j 'VfiKMM#5фt <,;ɴ_cl 'H3)vI PAܕcG[5#4e=d^&Wk M<%]%;J"5̑n<\@V^盭+磮C/^BJE;~}h:G"[T"PV >DZ#5 D+#|pGj|o PW*F@Y1#i͏.TbG Қ5\@%\e@5?RkJRHVvb+ZͶ龥|F Қ2MF+Z/eM*V5Y͎:75r|qP\ŧ)"2̓^y*gW^ZGaæ7'mBPfq`9"SܯxQ `[Umt,BD?u-W{FNUgV)鴨3 B^va "nJ29Nd+k*y'KP*mEd寶bo42j" lq:F?J Z;pNZHgi% Wʩ+_ f|A"e۬9Vo%0)v5|TB%- *TےDE)DB: -ᤠ0D9l?!.o\VѺ9A4YDS֍p`jT3`]Z35~̊݌: r&7\ R) K?s5/5E-OzReNtPC|ke&Pk(b&i-dYk<._YZ#k kؗRh6-)]$S*Mb'­WZ=̴O5MeePш GE,lgSL~D~6̑E.DU:/dw[DEAbXaQu?DPجd=FֱaAG), ƒ+-oRdf\ 2BY~BNۘawwp'0#v .ZO]qVi!S)K)ɵ[P B!t|WP٤_eWOsZȲVwTq/Q _yZ6jy \+ӄ7!cpO5j&4]?"7 A]wTCLU hc_Yvڲg[W KHQů)?F,n4Zo[ݯVe)7Oa=FmB9˵+*$Wamr|M{I㧀ZC!e47/1Ĥy)M׳g|cM<[ڏ[r˱;JB-#ͫ[]'~NQ-/ʚ!~'?!Jb;eFM!S! Z):%Cc8QQcdR[9Ӂ}+0 Ӣ߹B\. 2~/[y LUdm(/Z9f+*$Wb|i&V֡Q {@³,U~!>JQu2Kh*O32Sؤi|畯S;7!? JB(x$zY9{QG S/L񶧹CZ 蔖J*U,K6S+A3W}FL%?,3^4V :к7^ *EasdpKvɟ#}tr"MZۂG0@ߊR02Йl$59ƜG̠駥;J&VU^;hƃ(>A? (Wb`2vS9l9*!}x$voWqyY[A!Cd' LKL}fAkwO* s*Iri8g:@cvb%*feZ̝U 0imB]"p.[af/.!52~0 z".jYBr3: gʲ4[o2ߪ>^4oHuTfgR ,,Oɉm /$n VEz)Ws9Ye@̸`ԞIT?Yɖ[M\5-vߘ|" 9ge>5ZN?uQ% ɷYM`ڥHp δb(RD٥Бi˳*UJ6C+Cch2cf_LOp"ߜ,ϲK=k4߷r)G{5%T/ؘ?r}p.0xWϕ|w2;4J t W˱ʵu:[UJUU  -~N!0ߥ LJoXV\3\:s]1o@#ӝXmUu4U }\5cņ'\UWq7G1 7'Gӯ-mg"YEf-j] V'*J7)=@3ZY <ԨN׷0dA%nSO&ѹ(˫BGj\#xR1_ɹJ"Wn<\\@%H+7TWr.Hk~ƕYՈ)#:m:\u~+[ʰOF >DZ#5~ފu<+zx:T<֒f+2sI6Hk~Ʊx(@k`AB^d1HM/YV*JsLH2\,bڴ \.W婰G=Jg)^$Uql 9jעnf5[ZqOHk~A/]*lNQJoTZX׺@jy7 !Wʋo2^UĻaЧrB6 y).Ԇ4m"TϰIjxDbmr1Wj5`"D~ h+գLUK#Xaf,~;du4jEfT:g4ts84FцNi,h9Yb ]ajB"EI{R[S@Xz=YeNA hED^PtT9טN'~ghv>#YҔ>ӵ6EE &|!AAU֕D(+ubD;HO" M5T|UGB+ժ20J% wN7mjzF.@bRi RR jtCm,bATC(4t:-_Cp 5.bsQMqF"Į-C`$pZ jR8.R q>,ӰhVĽ2Nrmʲg/cWb`Ǖ[ KC,u\yJh&v W ]dAB j"ŝV[!:AnMБrZ씄bV)`ƇfRA!ԸPx'ՆT $j+4<&CATZ jd˟yj*nȬ#SʉL>Zrnl"`ܙ:D80>;OBQ*R"!˼'2:F)4+|DTQg++@ (,[[K o5R3$I 9TA(r" d+`Zk/`Q E,9 j~pƇ/K% R zQ Em1ϵ&Ȥ_b91 gԠ(f=?0)ex"EWYTN1ᆉtk4[)}t^UӪMpz) d=r-C@Hߓe wub@LH=]x ^ PVO~PVFd!fsbVA oR:*7@@ g&ì~GTDDsD@5tq%X@j|@1@} .fZE5Ć!9~Ex#0[ƍbqh͏â=EM^(aT.GҲZf? ńKԶ#;BقF7'&GjR5Ϩ_srKꈢƢO-OM!s(>ĩ8ʹd/pó泛ʛ!ĊH*uZCZgYCA/"gK-«. 2OcXXηz쇨V MmY^5?wttINf3\٠EF]1DKŲk|mdA : f(~7 ua.Snه2^AA,ҁRbI'A2kYA F1.6#qK?Pp}t!EU[.]"00̏@P" jӕά)I$˾!|+UłLn5 :҄H+&!'l iHkz>-\BhGTֆ-hA)7aB"FbS, bSad UQ)EIB4CZE6EU&x2NɨP* h0YO!6d[|#jy" sbVHhP"f$[yLd7Z[1d){TS[ǵNtn^Ϗ"1 Ѹ~NYVNתeHΚZP֙[ ɦ=ՓZh%we;g*+W]T:_,ITYM/P-]Ԛ,q *hHLH"v"53bI*_\֨-ƭbx.$*P|zA5c*KxF6@bU%|PI*Z..ͺ#+3[fSfH۬%M1\B3gzoP!ƔڥvU4!SFnAV6ց0JfZiJ֠mbL7a}jB[&.0˕bA HP:UrjGX!1:-!Jk 7,JqKb"VL1+G*6;wew;a:3zSWB ;dί!iat\>'3|fl*6Ί}r"VL.+G*6!?DZVWAfHl;+m3[1fH\8#igRo;gyQ0[Efh{{]3^UmKsZy:xs'(=q~Oޗ6/q(w[q/2in׶k];}^% 8fH0C;vvqŧEw#A:qSl#w$Q..[}bwϺZ׎0wyA) UmVMvx=U3T@6#Y:BJ{L;4/,dx8ҚIluIևWFLm9-w\؉O&|0zXC*- b9ZWO9΀I[9㾫o:2[Y{ G^Ĥd[z^eA? OACFhr!a]ę@ZS !+}a!L^'#R=HNZJU!RMR^-ɒo`턮J6bXVwq*FVW9Qڂ&^aut-Za/Ub r6fBk6m[]3R.yzg3/RR_~FRrD书8\y̅ׯr.< b r|Ac'Nww/y=B=1sxP@r2[Yst[}bd 1B*E*TrCc'!zB*şlJM;4`ymx*ƎU@]uiárcd|ŁT_XDJ ѱ/#+_ uW<:8}rA,}&8⮳r}q4yࠦ|s'9wŋ³rD[Q&vvk_|ٺC~9{+N]K',f+emW˅bDxArjIշL1*4^^VRg^=&i{6Թ&i/r/A3=BnXb+b޷, uMLke3%@tl$f+tҥ&cˠQU\Ѭq =W,pr cӪKbMՃ_Gg+ q|a8s({l5s+i̲ե[COS,f6KI3շ=4[a[e $rbZ:T xӊ )$!;9o@>@‰^d5"HKl7Il8q<8W 9k7拨>)eCl%] FE+nJ͞ 012c^JUW=πdϾ'*^/?8$ ]2sv8iRiylE#9qG[!c+ lzӧO9'>{孩>?_\,d3N [ȷ~IM.71L:qfSY4na Y//Qxy&wԖ1-ILN^M!K @&תP2)Al &ƒIg@X͟HPMbrbsP>s^!<<eN&S]u1'djvV#wo>/SbH|GFzg}Y`ڒKOS3[١at:[Lg >4hG2вGRTa֩=POSo}X~ݨQ.k>&|޹'_;x%'czeٴiV I-HX͹`&5˹" -X^$)E5*}liMRžn|f!*6e7IDZևoOFV$u^!igY[I"m[qq@5jm}dliMRžn|f!*6e7IDZևoOFV$u^!igY[I"m[qq@5jm}dliMRžnj61+D-k}˯l#g#pw#m|W{3]ci]ӶgKxhVӝgb}V!l뎉cf`ڪ. !l5F 0[ylV9F`fꢹV3`b}V!l뎉cf`ڪ. !l5F 0[yW;Ξ|ɧ':y敻_~al<fXwL#0ֱ᧭7j7ݩ'ywpq}Ǯ)1LgX#ݛ/nkwj|Wmݚ8xs'D2{bhޗ6/q.Eڽ\1kg#;pLIacПqN> IX^։;yv G\BA..[|:zR2/vq_jxӽG.d7=zLps7nju2VeMsm!Xi;Ƨ/!ޞt]q7:@"M%zlR'{u_|V'_Ȕ* c@ҭ걝'EL<÷LCRɗBW2t\wv?rc]$Ac+ky7o&Pv.4Jf8N984t3"K ֎(јn>ޓbFe er]b٥Fl΀gb@ssa*1+cB\x:y}_,{cu9"{fܑg.~˯ve v.~.Sluv/yw{rNG]Qp{_L=Q;D_~۟}t 2ue^([VމSD"U<;$dW/ {>s'iJ?ߒo/kY*Mp 65[xeAR'_Pfg)su\c %"7݅"|Vu̕#JDu9{ޑ?&[dVGեo3[fv8#kV~8 V~)[}g3/vF c+iBfv8#kV'}!^sq]G1vWz?qܥs;<.wV _ӧN'OzY3|Tj1ի-8u5ORDgO ON< WO|뎳],8*wLa1[źs@H [H+rb3.uƏn{)'@@yN.qbqoOvݴ]#w񔋻K 9"ސκϳ_>V]=V!%r3_Pӯ x^{ANupC] /Sř\V C !ln_V{ޮ]co'b%;ɻOtݼzi/_?^sp{XItdCb"5ӷDőo i~&̺${;w߻B̙gϜ|0T"7t[ $} x0$w#(d^k︌?Ol2[m]gN8Ia.:v]8u[UYvx%0[m]gN8V+H`JxoVf lu80[c#C*]u0[%eWuv\#l$fw]!ln^ V[qJ>68u[#mo]k3#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#' endstream endobj 67 0 obj 27840 endobj 69 0 obj << /Length 70 0 R /Filter /FlateDecode >> stream x]oFb+;R2|;UWjӭRUT^45Lx(g<=Cƴ"1&yϙ83%.s~^R|@&W}S~ ׊WC/so. O.DK$PW)$a[4MޤEd2}T( Sm:;iJjY]&lBfAMR-?\]5=\W>mכ&t[PV! x?1r~h4Ѥk !,-KmIYKT)*g{kbqT>K9EFo7x -SMs}: B7XGfƬ\{+9-*cjPnʱ[O%ު7un|֭8;4hsܷӥ xuOZ~N#|y~;(izA`XG*8!q-m#TBc.MQUU47uAK x&k EIQle WԔD鿕Y.1B$l>2F-Չa+uuҐ~*gzS܅hURץ+DIPK56UeE,oIPM6"[3&D d?˄%c۝z9H?vCS?NhEz[5#~uejTLFO Z/~76ԙ p CՀ.552tk-FG Hm 84x֦ˑngawY>ܜ #jC߱'no?:AQqXƇ կR|w.\rg2|zwboۀA5A w WYA4Žt<SJD*0+ػZ$"큊J.^e1~õ`=M;a _+!O4ۦEzQݧĮIm)Yg-bیVbYWCs[;P5b <ݍ8eSrW!rSZލGo%wpw'؀vL`s=0B @ < ˅ƃS9:~]k endstream endobj 70 0 obj 1174 endobj 68 0 obj << /Type /Page /Parent 50 0 R /Resources 71 0 R /Contents 69 0 R /MediaBox [0 0 612 792] >> endobj 71 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 73 0 obj << /Length 74 0 R /Filter /FlateDecode >> stream x= 0>7`բ{ \B)TpgEV JcϚwB?E,[$)+[Rw1s (cN3L3Bv<-gN'|܊" endstream endobj 74 0 obj 123 endobj 72 0 obj << /Type /Page /Parent 50 0 R /Resources 75 0 R /Contents 73 0 R /MediaBox [0 0 612 792] >> endobj 75 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 77 0 obj << /Length 78 0 R /Filter /FlateDecode >> stream xO 0+Q$B-GIcJZi[Q9 %9!M8cPZQ8"VQn)TO0Zp~"Ew'~ qB+<!;ՔiFiC\j<,.7Ig&V+M9de,_|w7ƃKR^&E]x endstream endobj 78 0 obj 201 endobj 76 0 obj << /Type /Page /Parent 50 0 R /Resources 79 0 R /Contents 77 0 R /MediaBox [0 0 612 792] >> endobj 79 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 81 0 obj << /Length 82 0 R /Filter /FlateDecode >> stream xKO0w HHPi@3$dRC28Σ]TI|><# N0/_<~+yenWG;~ k@8?{h5t]X?v }?{ 뿰\ՔO֏*>(b$ SD|v DAPe+~@SzBRcKe a0:ҞA!KDR#"A"Soao8J)v79#d/HLEJ|:ߔmխtBY 8V@[$c[PP"ɸOnAxVE@B+k>Tf WhL@5{"OwOSZ ewWnJ1^dtg쟙6l'2ʠ,y* =I﨟p6j|R¿E 4MX_qt 9)I$\5;B:y F tlRX+ EߺNl>9h8WXH+S˯`i w I䑢ihN# vÂ'\mJ?ЬT&{uh'UVÈw!il͓T#6O 2۳o=vZ]q"ӈJgRztq(LQL8 (aɐ_EۈQ-񟻿VMm̤f|9! >F endstream endobj 82 0 obj 759 endobj 80 0 obj << /Type /Page /Parent 50 0 R /Resources 83 0 R /Contents 81 0 R /MediaBox [0 0 612 792] >> endobj 83 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 86 0 obj << /Length 87 0 R /Filter /FlateDecode >> stream x+TT(T0PԳ4000R04R014R(JUWSH-JN-()MQ(+514ԳT47 p%*{* ʲf endstream endobj 87 0 obj 86 endobj 84 0 obj << /Type /Page /Parent 85 0 R /Resources 88 0 R /Contents 86 0 R /MediaBox [0 0 792 612] >> endobj 88 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im5 89 0 R >> >> endobj 89 0 obj << /Length 90 0 R /Type /XObject /Subtype /Image /Width 722 /Height 1388 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xO'MvW xqA㥱bY@0Z0ƞM-HiIãg~駦Zϔ<{**3K72"~8y7#ND~@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @}[ϛ@ <%hϦ0+O?l'㒨_7WW~q@  nMHY٠J z0nt5%ں` y]E%Xy#jDC8o8`D@ #Քh5u( t`y ~F⼁㼂5@@WS֡P7UTuW0@ jJ:J F<?]MV#XBq@WQ q^@ G)jP(**:+Q@ t5%Z` y]E%Xy#jDC8o8`D@ #Քh5_rĎs@WQ ѵ@ @WSmt~^{js`s@WQ ѵ@ @WSm_/^6֡8w t`]. @ t5%EW{?_ůK$ 'ku(]]E%XG׾@ 8]MquoD0_M>Pǟy8X5UTY8ǟ(DF~_uE& @ p"ՔhY?'v텄_w$QiP"?pI-P%5_dPZR>#1: #)jJ-~!挜˰|J6aQ@}Gח`Tou<90)قuGR #Քh[c֯t2@wt} FJxE"0`@ 6F)ѶY6wX,vb܎`"lY".p=WhGR: #)jJ-~!lDVc1p1ցCUK0gă+ H @8t5%L?됽IɆ w4p18wt} FV|9d%pT1aI@ l@WSm,J=dCc.:/huvo./^"-*@H @`cmu<5,U@}Gח`4:dy(apLF,Iu$WVqAd#q=: 2dVu(FK0gnPS `P20`?H\: oϹ<2FTzt5% u`#5?t<_ֱC-]_4:X@g.G)G}q^v:o$BصJ?}{&]K(7" CND`O Kw8N_\&]MgbM :0GdW Wb ٘B1<9?|:FU/Y"d=:?r ˮw:r<Kw8N_\&]Muŷ2beR+^]1$'_!{?i2"TBuѧ(&,q:+8}q՚t5% u  .E<7KuCO|yV1䋴lO1$K(Սx@h160`?HRXtՈ8N_\&]MgDü ap 5qi'1]_':4Ci(Sx8}qjM:X$G] p92%XL0OFNX^<4uS%_Y,蜋D֑|pXKq5jJ4?Y{,JCI{C>n32@}Gח0W[u|eN90`?HRXtՈ8N_\&]Mg(ondJ+7F{U;i`uY@ƾ p8{FÂuL>ZLXHq5jJ4?:rROX쐖J 2wt} vE!tdd4,XI|~EY:)z ?949ڸ1]M:Ě60eXa/˿K˜Ve]_B^Qxu` cl4b% 1 P>bp|}ln ֱ]Mg|~l a< M8vH2/aԄruE5,ȑA71LjtjJ]~!6Ց2GG2/^R(\u5-Mߜ}:ǻC1&ydKR9VhIKTcu_,+?J#ʇ  rY3Tg;5)6e?o,SrL4TX)o"/\ޤ0R/0=r(`HU}u zy6v5%4?7Y*H)̰^>;2ol &Sqσu.я?~pW9X> #ؽuXMK4Pp} ;ŏ4dx_y巆wt} F3,N { D0`?Hl9W#XsmՔhc̭ZH٣CY ` _w9İ*v~kY#qO^KÀ Ir y{w5%?`k640ɒ y/:\߰CDW];4g a 9`S8`au}WKmu^eÊQt↚qX#Mܿbj W 8=\%L1`Ũe%5,)֡` u^W!_/{eֱxpSgbj!a[}ú @X_up^ C];+#s}Gח`T/lNפ@>7:l8Xa2){IeyJMG q~ed3vF 8l\bLZDbwӶ~$rY]{wyƒuB2)!S:Ye(NC]ɯCv`~uZr :11qc|ť/a!hnc;,3% >rNZDbyAܡ]P*仴hMuE,E`lu&`(q_}M\kJ$| 3"UO!Q:NM6OuȗCQV:,TWeظj;%#xFum;-h=rl9YtEpK6+y ؅Y}$ɕ,!1>8OuB#g}h`Gglu#2NκQ,CLƍu=.:r6@я?#̂k>wI\<`9&G 8Z\>xpSab&p`P+20.jD`X^u@0ٸ{`{0,6 bu1 1Fw Y0cLIBh ~K!?+|en˻,$إHc|Q_tCw%*Sgw:==*/`1KbyjK\"Y%>D9,U9Yg*^4ɏB BB5~"3Pb&k*"x"ʄ˹\Bn8I:W6Oufax<z Ew !R~ D|AouMR.wt} C},n1:$d3jx$?%KaG^2q6E> |4<&iuf.ck@WS]: zp$2{4`X bXUJh;J@aēʗg%R.KS%! e$d9\/!\_X]J]x$4UϨU< y˯n^L_4=>]F bt9,@(O~U6 :;ר6Ou`>802PckY `L4s ;bF6ԩx;zD`BKi82ϙ0p$d3jx$%lUӧ,e'³3B$!HJf43יH1=NRUkSg1,Xԅ߮:Ї8up iy@Oн GFR%BDH\Xe^5ɃIGSa\z0-eN 9#Ͷ yJ qθhUy#ǥ\2k(krvv aI]_QAc-٦ǘ'XAxUCXtWPh.? @='4 t〝r*mYr'KVf !=PzWb.dLw02h;r\cb{c2nIXe^5 C6!Q]5 HZo{ wpb]tC8Tw\2Xǡ㒕)VP T &a" vg}_$MJ(!UXlbp핳"H +4bJal%qQad[(7ZH~ķ% 3 2?vzmljX9{dMy"bDx߷):7d$uP(AEUگco/A+3 mV~g@L6K!Izb W=z_*,TTyaH)[ ~^`^Yu[ϮDaYd ٠t6tݕX#Zbk4\<|p3|98zcCv6+$lkD`;T x.`)+1a|3 q$8N_\&zs%_? @1O-8sMʢ#5QmuEGx/˺o}QxT޿B&%o0:: 0U{zN:5,Lp5bXX7OYLW}?u֡P=El# ~12kP$2Ct̪Ȍ ^C;z~'?l&GFuGxg3RBSYbr{f $3 yR @ yFl# ~ցBL.C.g5aI==b磖^fb嶝%L ,FjoV$w#)6m`zL/ùN{;M_1J&e25&4i`9n ͋F}Dք.V4pց颿C(}d6[i{I`WQ>1H82ԃpXH<~䧏%CX2"vfp ?הo Pȃu876/6gD`a?݄dMd׎,zJ#{c@^-mOMr"wt} -}p+LCƶlLͮGԵȳUTK {2؟c޽'BW yu6U uaT^iQe_Z?^=cV*u"'Xq-lR%̶E"Wپ߆av^]ƁSf ,]_QUp&D68cG.w*gIak_BeJAǃa9T{8a '9Cclu 7A1(:żILC`' m:t!@I+{:MG{wt} m$gLO}F Gp)ދ7AK]uY8 LA89D"ma\2ɂ9}Sq 򤑓[EφB;-FFxxAQ.i5vM"0Y,Y{XڬcC02ϢV0(r$K0}Щ7W[;ɏ &y?p2)NJFD2M072Jn(仳mfN4r@C`7yܮ9uCd>?h uUIE)@췘/Ak; m$^k!'zlߴ>'Ə,0"=x}q4).cdRqOnJ6|C!?ITפvgu_Xl(-YnW72^ٛytvtӁ奏ק_/;m4}2aGIBV?cȗ.ůC>yaN:h@ |C!?['5S F#MC輒{(i67c `FKA;"!P/peȏ{rw0jڈQP=)MBXbnoru֡P=El# ~֡ /A.K\sn:F"B u/ZQ`M>|m"J;~'`XE[5 (#uj+ (&5mŢFVb`z ƨ,نuP6#*7f J7^NO/ȫoߨEB>yVdHKh54$)(}uL|Y_ns䕇#6 :uȞ2gYgX8p5g/toѾ{`DS_.mn0 [L}Gח0mmS-F|aѨ#Vy>?#`/W!dezKK Veh.£$G(uPu[dL$ӗ_߻3јj8)u 1/iy%ud;(KJФiU!D; L$v*`ܝ$*:ASUTV>p_fr* _>f9a?z <I:=Uf a9pqzᯡu$A0nL6 MO2x+Fܿu ]CɼlQH: 2~ց$s`BzbPq2?uKꓟa Zi_ OaRQ):d ߨUȏ]#/.z K"g1Zf .a_C!?4R9)xf%%ao:K ! _QrC`D)FC,)ZqQ6:6 ȓ HԏeE`.hT7•%͹xK_SDPhQ:H2IL}Gח0[1>Y&c: l$?4c/zS6"\b1gk*](,&OlPȏ:d\}%i(щaLBĠsH%0+2>eY㠟uz )Cϙ0Џfrց #I MVϯ{Ar 3nӉ1օ/P} ++g x$?SٶcJ>92Ϝ6!&1h(`8} 1zƁFh(#OC`D)F;"fe|3/ۆu:b HT}Gח`oD0x/GJ4ߨUb b´qgU49_Pȏ:h;́&7nj$3*^x y9[n7gDSf4:I%@N-2$IĬҽI]_^*L0qCuI~gۈ.7Yց캡u2!] qp@/޿GZz1Î͙uWO6g(1|9t C wG,VK0탗8t Je3Zu$? 3 y#|?`8>8VJ{<uhrpwO]FH `gn_H"1FCFղ6JÁϧz/Cx'$]_ѺUi_qfzgȃo}ߤU8OȋWO1a i ?a ~h`Hjbܥؙ AXg tn-@ >!Va $I ")CS:YM NӘmX =@nw Y/hl5%o}ߤ' k+bKw ݃G jxv.B:Ff24#.)-Io!'CI$` 1;4v#Ll# ~!fe p&0뛱, 7bųYr8ԽK0ڸ3vݑDŽa+W#_t2,dg X(XX ˗`<:>|vRRu:hƬg7v:Z,&\dzުdFD]_dn]; cyj| 0?l"ˆ%3SP,Fi(G`wL0%/n4U"ѱd/淥R27阆gA`a4_?,笾r @_1ɻ@(0:Fqf ^]x/akǯ|̜ah)<|(#'b28q3%a> $Pu<.j7 6]P,E gC`aހl @9':T>J\{=f<' u1p}Gח0R~}T/y7RW%y _֎kjoecJC!? >EH?3dӸAaSwS 0b*n:/tH91{9r+gS%heEۇFIzLez7bEY|Ifa= ޽f `%w N._r xIO[xɸGv+ܢ![DYF.:hGLy0GIީ,AxEʙiN2v@f|Ij0g(,*OޘEK_:`T`,DfҷW-큑B~AEAəKCV* 23]A˷E=-0KnQH:XGzW\Rw*HGԕKh43 {mu=rޘEK_<cYg&w7(si(a mu ZLJ򐹴ؙԆ'"O6g:y42oH/u/M౒3dz̻@~h}Fz;mM<>-sUTH5?Rȸ,ɳY!?2>EȮYh(asԔ3B'kXttY\bi5[If/Y.V+")3,sx r0r!Vt۰wkc֮)Ԅ#Gc0Ԝnj]_¨JK3P 0Yi_=k~"!S|JʬL4zOyG (Pȏ:@zG̗am- FA K\^0;7H:u B#Kxl:xx/=4u%^NINJ1]_Bn{l̤ zِs|N oT/%?3,Lf ʛYv&Vt9YX XG{eJ\LZq-7j/`Imau(EA$GDZCm:pǺ[Fޒ&8^Z?X%B o JqphΉ<ȗ_m{i݆B~>G(WgYԳGfgOdR ]jAmauȬkP@fɰ `Lv(cpuoI5Y^?8K(5UV)KK|?(i6ԣԧ#n+9yL[OY%si(a4I{9V2v 5I@`aXdb(.%&srցtO׽KjX?(wt} FCiGKy~9Q`/^BJ?$~bhg;Օuw AXGb`xh~CfFHSq4u3JFWKn"q2EցˆDo1(.%X@d1P%AUwJ]_:j0,a$g|FcNG~*vyJ@$k BJY̦;a~Klf9*R#Z X蜲)hd`d.a+2-(g\2scڐ92"@nZ5#)!|{U)m:lK*H>ѽOqK/z]_L{)Nm9Wl1ej5K}: #^̹P2K'$ A$p=3ePȏ:h).a ,y<:?t %DӒF 2#pREցҷ[x0%<#hCu ӣ{&# 2~ցNLAWd{7YW`ɾШ V.->(䓆ϞϨ%7x 8<6IIeTP̨8%7}YGRXl0^I@Sf:yR}9XYRq،u;ČC6%µ0LUWX?x1Ewt} Fm 2{1"/17sEK.{gCbHK+siN( ?o( k*c)uPC!?,8޽FN<'!gsq^S )3g̭,c:2@P9ZiGI40-F/ /Ux7s$?P2x'P:@I6k93zذ%Ƀ-(1T CG9r^s!Ք(CPhb` Oxω%PjݫǏ)q'/T7P}z 7r$?SlUȋ!sTZ3Il( ).Q)5#u7v6eIK0%X 6+~2J?x`. 2E1Ԙj8)u5^GnK)}rK: 7W\3]_:"$0KYuh˹/(*eS֟Y9rNPxPȏ:̰j bXaA"GWs*jk )3gOdS"sF -܆uEwW^i2`~[%L1~'&Ԉ K2~M~kO#?  y7y_%:MgUfH6r.;30Xk(G`4ZTj{<wx@ K-VXJt ?`,;= @jJht5 UlK0}"-Q.臿ќ%׈\ 0j 23bCU1<)74p©Cp0E`QXD]3F#4|ʌYX0fC L+%ս3IԽ{zBv0q@סۥ_beF<Q~GxWM,'G AXGLF~@ !DgC-7;u\ma`=[aTpqxaP2/69O:acF`8Ғ@󯾣K0*}i0Xo\2DV27s$?%KGA> ;~wT AXHJt8REVGQΆJ!0YGwz)64o ir!^&]P%Lk1lc`".臿VԁU"\<<2p*g.j(aSGr+4! Z'Yc%`1{ç̀:y /P@m:RᭁQ*ɳ )q P%A< zӾC=FD_+¥>~ AMԒ(^2x ōg>X,,#Քֲ& u@?,<8 LgDӽ nc@T*2Ӏ4۾iGp)䯂.%6#i| &AWhc8-Ō-8?'pC`g'B`aRU)Roo3ցI5a77z.S%υH 'N}FM#_B!FPt/7#Z**2$zh>/Sl=kV+&Rlo:x~Wm.u{y[ܬ@;KL~'c_ J?8cG@w\tG&Yٛae4ibn cDW,c Ho+u0(H;d+haT ' \I9%%U>/ˈn.2CO}m1m](HMHh,}>G7`j-OJHFcPC`gE`aX'ﴜ5P)o:xsa|8J5: Y9[/T7On $=FsNGp))'`rdX:^Ǫh(`,`3ˆ4|Q*YGKb3"0YZ_A4fcjJA0"$Qc K0}Ro4DI~K❬2`'aL2/h ãCgd2N:o\I~G.^:'//Qͻ1^oA r0w3d,#FB/Hxx*AKI#;[Ɉ< 18F$/pI b͸$Ҩ$9ud݋:EQQQhW/Vgo\?(ɏ28g!W1|7눘IfcͦФQ:d y m\XJnl%#Dl# ~"q1H۰4<Ȉnu{ȫQ ;uSd(t6T%m\kgY\9OZ GIN䡑8ß=0mɳ9 |_!@N(4[&VrC`b8)u/ysM9a04 ʛ5g.Gjvҩ{ ?/c QH}Gח`Toé/f9jbf'FsN ȳNo썻_C!ߗu0Mƿz\J-;R_C`7s 0YbѬfCn˵F:ug1 mM&)hΉkD:d/f6ʙ&5򃰎s%ɪ̐=SqpA^s!)3g+/$? ʫ4 jԽz$Kyj¥wt} FIKeDl,o4DI~K^w"~aSfR'_ |1 b_5٨@Ut-<ŧW^7vZxĜ m:[I*er4۰tȴVyLNfo5-KJcO0m@b_+ׁbLzVVX(ۑ(f^Pe(YPO@ UpXDt]Vkl}?`Rt#?4񛲎Ld8K Vҩ{J8wt} FO%V -J#_BDQDB3,kGÜo-ɿ;됧 idra)o?7IڗupqD4bfai g C`a|NJM[=Q+aؘu iŒVW%>h^>T߰9Q¥x'c 5Q+J§ٺN@C!? H^9hXd6W2gD`aF2wt M/ dHpֱJ;5"Ϛ?3vgH'0?{i99吳u)c|޽f$}kgxNJ xV?SLߣdD CF]u4m_3" c>dMZ jn%PuY*ku!mp 3ü(a</Ǖ4 4vZxĜ ma-`˄ 5 l3AQv-cfa·>7bS%m<3ss!/hLtB@3$z%P:h$``$W2oBf~;$,5kSኪ"0Y qH#Չrïrb?}Gח0+y.g4tIB>}1A>L_e/s3d 8$;LC忆B~֡U8PNŸٿO$eq/^R48 Ɓw (4cLC`gE`a1k%rvRuxs>OΒ\3 uod(Q Xoe79$"{O/RC(Bo{aIE"dl# ~ց$ a,`z 9]W bIgg iטT&X+9F5#`C`yYP3ـKP4{AJ)!F"l# XG]ux$K0yFg4DIgDfOd~ֱ {5(k ߪjr#l# :v/awgI#B u " 4qB*m%4ӥ cV@_& ^dM kKnvVN6cw v}Fݮ; Y0˻A$=(`E2v2d?ažVȹlR"~ÜW?kK=El# :v/awgI#ϛ509>yzv|a,v>~X1bXa4VU{"0ؽ;mQk'<+AjYj. MЌ~ ŀʁe/ AXGG7UX\) +>΄6cw v}Fݮt1|/S;퇯ӛ7x C?B~AcSPRbRgҴ[Eğm!XP% !$`4o$uqYDp` Y몳-UI{-?4uı$uL+~1 W(yàhfkwt} ]X*j׋G .!B u'7/G#e ~v~RQvexE3sB~ys8{56 C-<]_B6ΖD"T9IǨq CmқF!]_Q~IMģ_v/ywɇ L XT,2 L$ i?]dE2ͣ1 c,=i!#\\@uoozq&1[5"w|VlĕO ̙1/fVg0SowّRXaoRO:Xd"nj+"GgT{̴$041+\(KXb&ѧj(uwO^M/0%Igvz<#}Ji6i(P %):tʗq$e~4N8 n!3m(#_YDrImə&l'D6 w C-]_B6ΖD>"̓؟{yٗ])V+Pmց}_P% 촍:L4M`qp9Qm۶bQ\@Ek(;*0wt} m[,x8ulc׷/^(a)ɿScr,i^ ҽ)yno(6x2aiQIbØ~3!;F"pahbVuoI.;uoo zq&1[5"%_# D 0zZI>ØR)8$CϻpBuv,j5kwK\oq8kio}A9|m&w- -?"OYYԽV>S%iBMcgN^YA-:%u9 D(y|),lf-۬$L1l | 4 zr#&dI۫fX&fec,qLr=)XsNYYԽ0RW~RT%kQr0?{Ң'8Z`80.\6d< CiK޸a\N3Hc$㶃\fyC!Yc/sI,M D 5jR([)vhڦ:hMURd#k3")۫Mʢ5c/6}ϫ 8/P8%F~%3bH(b=aLLpff@LHXrpuL8H[dM RY ;빁wj¨ =>"bdz:f  IE41+w;cVwt} =W,xK?G+\z)-{,BY"`VB6ȗʲS3 4E aV Y#|#|@,Eiƍ+nG43txt$?X=y"Qll~ Cѽ0<*bɩ6hMcZeb88Ҟ'9%: Gp&y3Ր.YkɌe!2۬cG @`…6{#h8n 0b Dɶ0:' /`M.-!]"\@͕3QJ}GחA3h"b/(;KLcY U&0%e }JRL|`dn(6 +0zP%,|}lu4;6?Wws1T8ѧW)!Ei041+PvGEl .]_͜ޢxLL̢J )˖L8Q%*x~3ˈ15~PWS%T;]/9bʑW̒.]Ol5־Q C{ %`'yTrR%l-Ǵ,Ju b\3j!^ ) 2_~ZcBn??0+e=P% ubYwcC` C{ %`'DEwt} <#McosE?rY29>Ngl!z]xWmZ5|0AF f9#lM*@çlV41+ש";]$xRmn(9@vdCG O [a(89# f Oz+ӀXip9=Ѱcu_ O^ kbVuwƬR0(x=iQda9)Mn}K󅴣4ru0gφzYR8'w.wvU=#hպ&feQJNZ"aX%kQr0?{Ң rnwp1\t"K|:8Mv`%lC֑w%/yΛ-tw9fF;ɔVX9PFtw# pRSCĬ,^C ITDޮ/m5ΘmQL;X:)PY9+ujfVʎ({X 9/IE*N`.XavT< Ĭ,^C ITD? ;~3Jn"FgOH~ `9j5,/^)l%̃&s-Z>9u9gY@)`m;-M^} Ĭxt/a i"|:%n :P !) W ,=KP=Y;/~ sRF|r64|Yqޒ5⽺3|]_B&D<拾DSU]晄 yeʞ8`*WbZ|Oz _0!3,G`S|v)O~NŌ2!E :@Lgr\X$X5C_#{LsC{C8Y|Vg#D\@EݛkU* V%ox3 8SymB?X)8{-γv}+2 Ŗ),\7!O=Eғ041+PvS>?UmxҡJ[|sˑ )3Pwl`dW ޲.y&J! w@l =qc]Q#ryU͜& Krii Sn<X-3'E@bcQ;JD! 5I@ĬxtyTKl׈tJm4XobI4);r&rȗaw۬#1l^¸U0bs[L^(֑2܏n2LKI L]Q[wĀų.!*EY0ЄX/a=24;HN!FP6E\Z9O.4rukÈC9?gi,!3yX3+OS8]~%+_&ySDC`gˏ!pahbVV^C!Lo|eKl׈\|p} 2/1N=n/{PY䍆˨.],JXM b|绲GHP[c#;X7_qi5v{D@ iw{ %`'9UOV%<5?l"58o%_")$N2b@.lKT m(䋬CpcbFP%~Yqe|2 `IWl(4!-h5W-!]"\@Ek(;ɩ"zKK}Gחл7ْ(dH|c`,<$NOa^Y05[@C!nl}|>KJXO`r9~shbCW<(L%ߏ~H0V3ʐ_66/6gDĬ,^[N[x;m-x̖|Eɗ]㩢nցMig' "E"lJ? qL~e +o?|]QQKÆS.9\4vZxĜ C{Uٮ 8UDozq&1[5"% ~ÀQ] T{ZM041+ݛ?SE4M/^&ѯz|^ӂ_t'L^76űpxFyuGf=b+g-t"# njbVu$Y}Gחл7ْ(ap^FEII&Ou ok f̪cG !%Tw q:H,hrOdcT4w#]\@Ek(;x wt} OvOG}5[“%e Sw}~_G .׏u@)uބ8NnA%),vIǴSYy)G'~P4jɷwpF}^|fC!:dI _Pbq %~+:VzYhö`j-vYC`&8)&fF!XEؽ*< VxY?|Pڬ#W`GCΘHit@_E+:6R") &fţ{ =`$POKxBD Mc&t_ٸ;?ZI>`ñqqRfC~DI4q9'b`~^?Fˏs^SI!H|97.gIdC`D)041+~kh$*0]_B o"/|V.[9VL 2/Nɗ"0^)|l :J#H:T, ^6_i˯쏽J%]d`Cq!l\@Ek(;ɩ"z N}Gחл7ْ(0zZ{bX v6# :܇-A5zvA n4/Gf@|Ezd63?]^MC)\4/pn<&feQ|SE/wgo"%_#rQ` 7ٛT:%5,0.w/;4٫4hO` cJS5ƩRR ~ ^HO83,FM1!Y<%&feQ#6TKl׈\|j:"/Θ> Nگ[PmzS!c˳N`aq:^G_8lp^ةc1}7ix0 G71+WuڀSE􆩾Kl׈\|IVq?MTM*#0Y9没۬TIA GfJVaG#Hq/̬M_2#Cf;v\ C{UA) Sz;+D-<]_B6ΖD\,~ʈ'C(%:GHgIY )W137vG[7AĬ,_v&iR%D>.>?7)=Nǯ%c8}nC!_:V㹄*~`I=X u^ 3hZ|زcf3X>x. Mʢ5TeKl׈\|a#\f;%_nM@6ކBn|!nJ NQT"S2u9q2퇯կOцܯ!Fܴ!&feQzHu}:[T}Gח0[ޑMģw%w,Qɀ꺆%mo8c6Bn8a ga(Id"'}|>4U'4U041+wQ2UDW6Tx(~Eױ❒O6\;{AloC!. J<Cm2 ,~]O), a :*"跞ˆznyYYԽgTKl׈\|޾eb));AA<!{IXy9pP?pY6d1f29򤶽pRV\@E;}1NѪCKwt} uo"]koK,Ivc24c0ԋS E^^  IÅ E0 041+ކ}:[T}Gח0[ޑMģw%w,wgEr6Bn`O||ԳF@RB!Vz D C-3]_B6ΖD!ۯqYF0ЄYN=bO**!i(^1uVԳ}\Ȼ;» )ۥ46ȟTTB f,-`,i ,1b#s +g 6șFK9  P(rS. aVJ-:r| :`i-!71Ο]N3Fa'h2wpC`7sܨ0+dcTIE%cԏo>Ǩ\6Eց G,ە?rȈf.:Э핳|2[> R0+IE%Cw1s,仨̓ck(䋬CDڷ8␕,#1r>܅!6%D!~awo|C`7Dfe:x;]$K>-$WwjP~!}-@h) f@`Ѐ6=NA Zj#jHR# ݒZ%RRwHЅUU?x.N]?\c?nasmGA%Y0+9ƎHOg6^c6,ֶe/|={)q#pʠ6jԨH=|؝u|xxUKm QP}A++$!ҌNzԈ76A#l;2H/SYU 8ekejTUz:R/?2xai& 6) 8%C:T}h >eG˲V7kY/ƥg\88~7Y3ktd_`)"Y:]E%Rl]k N@ZYC1m6*R~W9.~;} *$`YV2w#!>C4:>}%Ͱd*x:ІR֭+.B8)*lTfN@ZFU޳x>xȩ/Jg7s_js"Џua3xLҸq[FIL3[9E͹Ԍ (^+(X"գց"GJIpt+RA%Y͉U…|g%sfXB:IΥ9$:H(ͺnziQUz8}JQ]<7{| }዗Tru0=2 |4o|0‹>Hәp ,F?JDS41C!>F04ES6ҥR IFUEwk *v !&8җ|҂JcN/oäF4t&YPb5P<4$˅ X}Q5]mn (^+(L"գցx-#M:SP}a3,)܀nxjHFd lrEU`o^:*NYEu]UPVѫFUEG%â>i~)>`XÜ. 0 0b8ےd7x =:+ \SEQel_L֨H=~uǕ+Tru@!X>:Nĵ* |#d[9˰+U.`m׽?AI,`꾍u8smbe6Z٦/Udc)qS_J *:pαc|:\\RQD7WpZ 8ɗ be۲[r-ԵP~jکu0 .>fa%vj)h}AiL?8QPO$N$W,,_>'|㋕GLB:u``yFԫ N%_=bH\7q'):h9Z' t@6GuJ){/_#R!P b~G:,^LJj _=a6qό dbwUdzH!򉄍8EEvg+1o|2TTX5$ K`bTZ[!0oRtC`"G1FD5=XGt$ZT4Xqd9H2WZ氎U: p ;2ã H/XH/VӸ}# wq%`݇}W*-hwg "*X+1gә黖+afӿNiA{:lłʕ~[95z׶qduLb\ܝJX|mE5wJ Cu Bh Fu7irNYiTpnaQ,VcVK+C7,b~W:Kecv9=YC*&>]# wF!u~}2Tx/Ic:}V^]~Bn.hc%nBBc3_Vru]P@BU}X}狽RiA{RtJ){/_#R!P b~GT:gŖyr`|*;a;lsORo|2TTX+E\Z:QKXro|2TTX^-Nӿe=lu\>c- X /W 8Y@r`ֹ5-VoJ lW^XuGquq⹁|>:,B)3,{sj.v1]428C:ږJ%JVbNneŧztƞ5Hw _f}1λ=[+gwWA fՂR4ծ9w:B@ׁݍ3Jʨ5!:-S]bA{; <02da>- `wp0q)=R<#R Jە|pt@ t}yM["EzꈂpwHEx1sp;3, Nc#C~1c n߮A 5.`%MN O}X Ep_T:(X& v!4z^X gubeHQZTX55,w}ͯŸǐN%_=u7خܖU&@z.`m23V0=ҳtŐciWcZ߽?pkAԈ{ka [Uy,tbL( q<ezyMDmu "%]_`X+c_?'Kݬ(hk`;DTIUƯTUvYGG3, K,VN;:u" K%#ΚYތ1v/|҂omÓf];wͣR/XI;ZX:TԉX/F`^=u^|6rts)ʬ.X \A$#!-hWuB eiYgQFg3.ٓ|Ƭ 9g+Z9LbH\VڕWb~gW:ӟxBvbTZV2`b~n]5(ţԟ< +1bH,]7Hj4|æou6 zB=uйr6*7vga`幙z1c n߮A 5.` /^_w{ Q:`bDs*/Թ6EJ >JbN'uVKC7:~o|bٰrIC0s@=rA]uDZ]@Cmb~us`I2~8uY W;irgr\LH ڳ*BA]gv%_|Ju)֑S:xE"߿|[/w}Kc1븁vn '3UUo(6v%]u:H)h+a븾ݯ[/ߜXbL:FU jӮAPS'X߹b)>a%QnlKz9iŬx;ǃcKgU.9jPNAM0b~u` 72r gyϺ+QlF0̣Sͬʋ!dJ^ 5xs'O87dv/hay=ֳpqdדڋmu$>|;O"[+/t-3ە|Ktv@:۟us{c{K` mvl YTUYG)9#)Ő5*+hC puwֱν XE"m{ N)LΫ rb`[g-$|H) 8Rmڕ||t@:Nܝui>܃W,;)QT:kxCBxŗ`T-^ )FEƯ$-u0RA$R= )gb!uܦ]ɷGWi4\YɖNE(Uc^sy:>|_GFm4jTR V./"Y7OՅ]C"!P &*aXiLŸ}KXAi,hk.Vo9I&q$_m1u0dq9lx3,a$M pJ[Ő:mnSԮ࣫qMx2}}gps[;jۏX6 DX0MQ(]iu r~N3ȬܱŐʶefo.b~U:E#1/3olQiA{XXmsh>ob 쇱j3sxzOGEa%]b̫ P5G^E@^ l"95/ ^ŐciWcZ߽?bNwW: /kj,0+/5^ـy: %>T 7He^ "fd,ia!PR{QC9~[? kDslbH2]ɷDIj4yr۽RW:p'xtf A_ya 0^p~36 ֌6:<Ƈ|j_PuP#o|*P90ƃWC:ږJ%JVbNU:0%'ގJL-ƴ|?c aA-iPLo}s蹃_f< bH6)jWmUZG p:I(p]=u1p4ӆ)M *׏I/uhU\e ~70L] vŐӏEۮEn_<hX˕1<8G%83_=u0oUpb{# ĴWf lv]BŐ]kT.hXgFvo 2ؤoCGV-Z91a sSS-||?)bHWەؙ~p@6Bkda,rw& 8!?bH,]7Hj*aX^&jqp]/[ZVJ\iﵱ G}qwB+‘8/4Յ~N,J=Vbcf).s%6d'dş3,Bl=uA0:﷟c@?(7?Â0D ^v% 1]-p*a ,H*[oOA{X H` 8 ;4ϲ拁?-d:Y 6EJ >JbN'V:0ʎoIG않 JXG Z8FPJN/$YGǸ%GMSҵzv%5U u8P 븺}̆x7.|b`Ou\]Bׁ ag>3\ =qv%1 hX apЎbi7-uנm|SX:iV8X7<f;ŐnW]EC pzaLjϲ0aR=H{o1|1Xab/4/nk>ii'N;۟+X-"u8Wfiad:bTTu}FYJdF0w3=O1Cigv%+]Ep`l!S:&vnȢP#6:`7lraa~2Xy)iz1i#ە|t@:^u +:1{0ȼ^QA{(֑vb`e_Ga\ igDhXe~`'Iw\75ѕASb~/ejV7AzZ!{1N+6*#u8X 7so[dݖ븺<_}ye}yY b ĴwbHMmӮ5NU:#1%lRC{P#+֡SQZ4XqX},#+0`c_Y-wcyrC괹MQo:b~:d\*8V`~A{XXGA|7ضYowsʮ/3H>fŐnW]EC o :D VA{(QvY?FpUӰs),4`5,eNMN jUsڕ|UX wc߲K C:xU!aRm#f)^ isڕ|ctF@5/3#KC^gf_ ֳbJkYcrb)&mu@9[ae -$$|sy} lX8f*! wl l_љߗlc_ :ak;)V÷o7|*S:y1inW]EC k5(fy k' C.@c=37`>91`C.v%.]9pתbbi-]׿L\`6I/isv%]up:Rp C":b06E)tŇ,]PWӮ¢O+:R| C":p0? RPRX:iJLz)oP:5# XGXGFb`fa߽+9lSuQVO*)!6b>b)>_i#XGb`e,&axwISX& JH]P:5# XGcڈ:RL.`s!Z%&I3q~ )iVCWZ7Ő8]7Jk4\ޤN߉ubEmuXDtHX9kQI 4),4猐F jGOە|mdi XGOA{(QضY'q,!8 G K'b[#!6;ӂ6A]7G8:NH)h:. F' ~Au/ƪ9J*,jdR#ŧ=(l#4kf яݑSPWf7H+'X߉b)>XG`i멭 >㟂:~UJڕ|8! wXGOA{(Q: ŗ%, NLaLu5mvto.qu(֑SuԖ_tpJf c.iە|pt@@DPu\]C3X=\8eOA]JJ j;T#ŧ=(a zvuڧJ62j4ӧ3Z׾ <3+Z͇+!@6u0 Ha w o{Ѣە|/͜LuLB B*:AaY[SfD]| >껱/H` 4s2mWkw]!G@Im9,Hmoawց??Ͽ}9'G&:R4# Q-~kJx.XԀӯU }Ϲ7KN 2Yk` [q_bZ_lr_D߰ns6t@ˌ tS+y#u1-g񛥶Mv-YHֺB?:PWh97E.AD.\XRww'+)FojRANŷ:x$ !6R!(kf9>_8c)@Q “Rߩ]ɝPuD(M,~T{Gv.IA-S-{0:14uF*$۷q߼~¨tOgQvJt@D@#BQmb;Dv-Lc#~9-Me ?!? n~Affz]Scu!SO7K=P$'[doZ`~Q> DX?% _-pmbA )Uv%wCEB " 6RU" &Yu0YE>f0տ udjB6:j둾<,"p.Ia^ V†5cncpdΰZ'҃Y0.NvG ڕ XG7KwT`'[dP׿O>5F9I,7ujK_q(帼~eia%9 Ւە|#):utt{Jv-B.8c00zH&Z^I6xr W#B|ק(ە; [:n7KTd'[d bˁ'Gؤa!,v@o|w?*C)b)ub uBـу2F:ؤdu@6؇u+,fa a!i~}M5=p:X*nW: z//D,:q־L7̀_b7#gvc̻#u7m{] 0+JPs]!iriݮ|%XN%E:T7Iht#{NLŴS!u0X8L射%,F% u=mݮ P_u6oE 0:I` Nm,_DN&)RHX6km m|PcNv%+!:p*)’ 먤;NCIac(^ZfCdw^]ܥKnWzL߾/ubQsN"0pz_|*n\L#ek2-- [ަ];BBXG O:*Edh926_8Ig $fΰ0vGyc:Bv%wCEB " 6!Qm״+Xq"wEO9F?:lLܶ>gz'1jc]]BB E@#EδXGҴT9> ;X9 34'u@Č Ւ@$ }w+*E j]&Y}o4t&`}p ,:1,XLuAv% :buKRM%)24:nۯHnW5QmbXG}ԜY]G+M2S3YFqlݮZubQsN?p̔iL dmDS vJ^i# QuGI8:񻿅n)]ZIB@c\TӾ$Xۯ&^5+a<mۼەTߩF@먿p.;ҝ?IgUt 8 S-0-hi.;cr.Bm!0:`ɍ郇ۿd%ݫ7ei:u0A|,~s:"%I;mSvǀs9ciӧۈ*Ès1<);G$#6?}@&6i`8J]K9m:_@0 N&9Պ-@ u $iܓ^l;3Y6vgAJ;I]uBwA$:FHENoIY@,>y-ntV2bfK9K,q*[ǴnW Ib[!=:F6q,я B A`ufc &:d#f0rtzjgGVΚ0Ќx05ד&}w+yt.B<وF?>Z"0:Y94dLo sY5,锿|ӿu4}^# Qscl!DN/Ɯl 0:b;|N3YL60pbNM*̶w=mwĠ]NBXGNְ'?qUJFab23uY9 z]0pIg\:<:cTK'):RaIRR4XG]e+g1* [A`wt!3,'MX (~p%l4u L!P7buύt w(LG=Պ9#j?_GS3<Q =M[ڝhWV_rX?`8,WåV@IT&:W2vhz _po}I0oy! oR .b3YL' uOڮNwHD:"ݍT{e|PfXXa & :? ,eЮ+`)St98wTp_ ^}wņ91옼/S?@Hs:vu];]S!0X ,1sw%!!{XC/]]mW"FNO8)=+vڮ>|K:|O8)=+vڮ>|K:|O8)=+vڮ>|K:|1?Kvo~=-JRWC@z3IY?Kvo~=-JRWC@kz*pX^/=<JZ_W]]mWT͟b~GnMO5b3PbT*{7b/mYzڮ䓝 B:|58&>[N!8{t]>EwW7>,# v%"zJ>*wua18˯cz D.†navADSʖ]ɝPuD(OA!7˔= /Tb4#>:#x1unge/$F:z{ifnWWە<_i!0X2_q%ܲTpKW͌ Ml+`4ş_!}swS@lZM3tڮ)J 1:Ɛݍ/ƥvWl}%y//~ADJ01Ԯ+zꪖO 7w7ξxn zkKa _qiWWە XGb0іqܷۖn7nLjWWە|_[A@勇/Ǘa%-ԣ̛iǗ sFʇ9SHGI̅uc}9)= Rb#j XʃшkU^!Xާ$XrXhWWە }:9TӲ;_U(%xrS[4U$ u>_Pz|kvu]!G@ISqacl.jWĚVse_ţV郏 G;oVVv@<l_oWWەq}:95 s 2s#i1am{ޖ 'LSXHXCHt%{xDkPlF-Y @:6lv%O;Hi!0X2_qF@+ ѱ[3m$:(`:+w[ﴖh4DMl8%] jH!ԴNek]]mWS sZq?8dcS2;ک SG;Js\>_OiL=3?:f8mՠ،u=6%aH?{`ŷtz$~]]mWBA@sgsڑFm ҝ:L:9{D5-!{ /4hF9,j1d&W?jJbwX j0F$p7wG <@ ᇡh/U8\iq*2,NCQhDp kPl@#F*rÿhClRJUW䵫Jt@D@#B18YF$͸d&?',8UK>׽ͯD]Vl{$8`0o%RTuUcPy2u]Yqr"-iF"3 8 Fg`4TZb3^rƕ$8ܼ>'?qoWWە XGb0Qq606Oga]X@%E<9^:R/2u=64rob"Χ]]mWr;T$"bD nG?~;,3Z&U`̶,{ VQ l8^X4oM_Vs@v%wCEB " L`_у9yp_ t08j=fXUl6tNXGX3 嶿F?nYW:u@'Xߕ5盥p~ Ds/Vf*o۹YavOkPlCWmiRH( M1X2v%wCEB " L`mY ZMB?Mwp4cimVe*I5(61x7jwuN>SXv%r@>&iN%ņtXrhAV#ڷfU=@i% 7~#!8l][3>NQSv%oq[" ]q{?(8p :,~A[*:HUTbC3OA0&6u`ޮ+*4Ȋml3%ۄnbö|0~r7}X,T=ti >x>GR 1o~ ~b{.}:20b-J>w::tN0(!Fo CfC'ezm~ *k, :aIFs:!pXV# wP  10g?0?~;jMSi!nkA$*DmVer6+w]]mWIb>g 8 ::O[Nfdl KE!Leai1fV2aqG?K ҟxb߻]]mW5Jj0F$~#Vvn6<``DS'==Lh:]b N~|X-w~Ci4K]]mWPC@T 6܏ވn,.:ß;11d{!WĘg#@zQY[9n av0oWWە XGb0Qqg"BG50A.Am'Ԟjeaxjwņ6:F07 kXbDJyBr:8Gb  sjң3JjOcQ C`w6wV1Z=6uSSp*nwwl1 ?C=‘v9HR-EJ)c4>jxw.ѺJ{X,-'VזC_g<d<W^(_5hu CyڮNwHD:"ݍ3f98d`L}f,IXf8e nQK>'{8$-f,q ~J8+<+[fڮNwHD:"ݍ.cxm`,u۹p&Mn^]c3hb MƦp{F9l$cj]C@c ˯83-72N L\]Py,4m unX!BXԗ=48(6HnEc2gknb__M;ju@XGF?]qo1aY1;a`dC/~՘4ǰ8wnÖܹYTbgM,%T/J+vv%z.b>bgۡ&Cwg>{ƣ3Ʒa `uJE(6\"ǭ 2Ƣ!ǥەzuVboR1?v#hCkFi"/{8Ls5(,",[ƅ{ڮNwHD:"sv ^~Lwqu v7sR^ V:L,,rvu]ɫr T%b~g~7 `HӰf.HCN&)j;lM{`ؾKg)v[NM8rX}tXH쌣j H14XB?69gsl\66«"7*5,|FŸ7#S az#{8Mc%5?@Vd:,e3خ+ycπ ȃuGqo!Q5Hp~T 8 ap@BY1No1Cɶr;i5; vs Isڮ]j_&c Olu A>fop@HmrLgl27u0iW9O",Zaj6%ᯎ=IŎ:78mz(q:!{PCxU(V]]mWQJ# 1M(4Xl0&|}'1=8> a^r:"&eǐi.RMRl{FaA$iJOQ\fEv%;e b@:5}l /"gqF*o0 !89NY&_}q0_p3'{Pis2g)6}Kp 楦y_\(Ю+`)St@ÝyN{x:iX?fR:9μxe*SYp\yR;:a)bA Ѓ: 1?Wc+e ;h8Ł`f8f!٘7HHSگ}lͭ'aBٛϪ4'!{Rur{LuY͌a`D6g3eiWWە|):utfg,'0 C0 |u5f/DΪܹ۩S))Tl` PWBx5 rb jLڮ}L!A _G48䇿|b4ifqfB"`/NLӳ*_ID|N6'VvG|ƦZv%?{XٙGN0,l\®26m2R f*vUc C0o"ͅTc.b/E@?8&DҎe:f'7UٿNXGvOs99asG%, ;`8cI uqiՅ͝U&)t;I =Mt4A/K:7`t>&5# Τq(aPN󸢶Hfp7i)Q #f"/1H]'-VѤbO 2-m.Is:RX#}lEdg *DA1m{@V>ƀ N lGg4>?b4V2؃J؆s+AOXGB !b~7MglΊ HQg%LL* H4;601CfLmbأm`@,[$?QQ&2 mtZT`;=iJIڮ^P# $͙4l5k/kb*΀o0ganj,"&NV YXSmƴa=)"{h=1cz;?Ka9N$v%OWZ! 18j~GHBx t`gg.>~0U0fAzI.weS4N($0K1d̿\^p}p=Eunj;ݡ"!P &23#܉_y;c.@hoi&:d1?ާ UfAS?ѐ=L~J|q{$|ڮNwHD:"I Xnӿc&z ֎2O*N毡惄sdL 糞& < I:~#vY5s/2ȏ7+Iq1d˟T1?^@{ )I 4үJ⯴C@c 1θvX^ s- Vnj-xfem.?GJ<{;+䙚rڮ:H-b~gF6XGЃDbv*AaGg_iF+,AY:0<8=B oP]NBXG\ `oppYƹ#Yw%?-9_ǫ#۝d;{:WԸ!DaO zɱ AH7ABڮ}L!A@Ht8?}v3Ͳ0ӱXНjvY3iJH=9 g6%a AHsP|+3L-+GhtR'Xa:&JEEa&TNiWWەAoXq6WR|t88#=!~%<%n,p~eC_[*T1a@93a, P]]mWnU s:imN&qBqYc?X;=AQ 7D&+)[kqutTO'!zܥ~bvJWB8u8P4imuYƙ_vXW*Ot35)p2edn*wua14:{Y" rea\ۼkR,cؤ:T4&;YYp(cncDϩo ^v%?GEwUIli~损س;E ~=Lm:=cz;?KÂ0d:l&8]Ю+*`b808wJ2Ω){fA_̈́Wj#D oMS=G2}ha88L8 b˸Sj;N9BXG4g85ְ0aA!Xt*k VYw/84416F(ZOL*$:Xc!>Eq ]ԖtM˄l! ӝhD;=e! wq6@1Q6,-ˁB:Wj2YuI8`AKP!̷8Mgs:e;{:u?Q0.%ᇽ!EQMQft2v%tN bILeczvqq. 1}7@߼J*ۖVA0HIŞϱ~bƊӿ]8R\y`Ѯ+y#u1Is&3S+gaoƝDqࡁ_S'aGg8b0Pjg΢ 3FQ⬊ _3cIvitR;:K3DEbAC1iWWە|7T"b(5iY7o Iz{jbV1X2eLҩ,{VѤbӌN5XkXPNt;'}nWWەG@Xqc67[>0vlr8mM*vTԹLf. _CN3COOdJ+֑Bh&s{%=Y 1f3g~]QY7Jみ;wjum5cRv2?S<6l0As!:h APsIroWWە XGb01i5,-ϳ `Ö/ϙC0(dm2NНFIŞdc2Y02/l1Fp1|h}e*vە<_i!0X2?i:ʲ!iDh%8FMB204Hv}=javO'{TLC7|xUXڮNwHD:"I `ֆl3$8ey,JC V3jݿhi9hyGȓ=PTIv1Va.AJe8٩vu]ɝPuD(9aa6!50ȅ:!&!8WM=lfF%ca BZ:b.-{8$-fN*_u,@]]mWݤC:N4ΰ4J/z_g~YYna0x` 0b%:ޜ4̰DĘe 2tX)A<Fƪ/{VѤbd,^vu]M|Ju9iͯ 8ģ1+=8GX)3ryXep*T1̟+֮+J]fO C's=iGcJ2;Vx甲p+%vd#'&{Lo'g)z0J^oSB@kG I^8x54 ln  Fe*ćAoX9aA6=Ş bGj#+G@|+Vxks=Ű %;JKefY6KBڴ2i + p2{Uv%_C w\ `D¸{(u>g  =gѫ/yNl7uN[Es{L*bjڡjdr8?}va; u߾YY2%B0PF5:F̕Xx{c_:z`a:쯉Mܰ9ܝ=}@ OS! $~ʍ/,e%4|ƤOZflؗuD;wuPc\b~Maaõ1v1`gaBWr{x/$ʲ*C@c |=e(d2u0'b,$+>MDBLp%8f 8*C@c |=e(d2uf`!])k`nf[HYGg=p]}:Hh 1d ¢̓A@Lzؒuz?:E{<&%ƈUJG 1 T0BD>bXQSO,T9:.dhҏX!34;9OeCw(1͂KC@I+L"M0Ц9:.E9u ͰUl #Gx?.1͂KC@IѧZ :t$:B6U9+cXfD@#BS괋XwF*p ep@Qx1 _X`v}s]9~TNsA)SP$xc_x-+8ތ`܎c 0 I0yts\a)$U;b[O,T9:.;u t7.pjXDt$֢Jx^#{XՌXǬS6 .Unˊc:?f8j+/Ȕ=\"1K͂KC@ﲺXm#{}==7:E0>{/=J5#bT]:a[IOiX[.eX_JeWտzf! w$x) Fm-eiaHs,erJ` oXeBpKOZbRO,T9:.d,Q fF3up&*SQ*{'ߦXǬ.S6 .Un2YTbsKSYGϑ{!9enWpKOZbRO,T9:.d邩sHFI2VCE| C++i7+1͂KC@L!4uP/O됸I Ke yzf! wY&` ft9u,bT*{ݬXǬS6 .Un2Y`氎1Ƃ=o! NsL XǬ~S6 .Un2YkX|!cED#:OpdBH:fYprsu]6:~G+Yc YA ְҡkͽ!aGc*mX,:f! w$ ^ D X/!\MX\X2a8@$WU,5}elnV+u=:lxe;9:`\A sI_3*X k^lC63Ke3RG}{L%9;qu w#ŧߖ%Y ,p"eU NŶ~xlϔ=Qi^a"_A ¢̓A@I ipxĸoy f#$f_XI@#H+=<{u9ut=esS+u/d%ol5Xxw0*bQ>BpO{/=d>ŦT:c?9u:aLG xt>7ҌNb qֹ:8x|˟X*]ԛZfENפ ު>G#]'$`o9}rp`úZZ a-b멇ZCiG kDցwG:X߿vF&00ao)S=cukS@{szpTtu:xMscx0JwsKe"v룇c {S괋Xwj_<Ƹ'R\Xa҇# _*{j`!+:#G# $Gn:^`y_*]00 bSm:a֝c{awdЇu}ך  .u: uN_NB./u f!;X3k:^< 0)CuR"` ^իus;La $[[cd#guwTH}"IӬ *D(=iN(=,=j=7=#nUO w2ցGdcLA5TH~?M@Q(b2 PXcҲǠw?k; ~0͂KC@I/8^-(i2Fd81vF<bc S2>Uulem.$2d,Y]KϤ }ְX٠د0fX<$ʃ->>Uulem.$2:3烇Y}!ͦ_}8Z>P ` @+b!_(f BB Lzc:poL x@~H 2eW*b//<}.ʶ戭"! b>왬fXV@BE;'HȠ6c5CX+/Ȕ=\^_u@^hzݲEVq5. 6: zNu̎C0҃G(ax,^8Xl+ځCHlȄnzX.֑zR{mu 2 c4b~~b8یnt:PZ< ABZ$Z>e[+JjCD@Gf_Z|5\XGxbztakߩTp1tqw+jcen{(־blkk߻s۝u<KTđxr"BİL#-q6mIee3#Nj%1S6 *4X}n|_/AXd`tְPdC(cRgf=Cޤ4DO,T9:.۝uxZ$mbp+~Q2͘vzu]|3-')ʶv°E@JXGk:.>zXǩ",O(ZR !P b~GT:_0f[fSξ_`FfL͐/\mm3t!!XT%FH\]@1f;eN]TǧRͺsQ@Ѕ@&b>P jkXba?|8XBDtr+/Ȕ=\^_ E6Au@U:X›?xhq 1(6{Ά:2ݟL,O(f BB X9 D\' /?ǩI(ݓ:,O(f BB XG`W~:$U8.%{x~~U12{XY-ϩ\(ZԄ(Xg* (vtp,~,aBQ<{/=j^B@5&i)$gxw\;&X^:cp`d?SǧJXG`(- ̩UCéRpaztԇyĨjyVET$vA@ÇdjwAl_:w; }d E35Q$oM B>>c(?囃5ɌN}s}p֛HR(z{! #_ xy/҆0ohI3>R}vlB=fw{X18} Ta'7Kǵa2ͧNɼm?e9rlk]t.F@JXS3.Џࡗ|7m#rTR(r ;F];qfPK(x4g2ǃB ˼爁xe|!b>isG;&T~X~ x)>*] 8Z'9fI)\=Vp:?qʈW'mm = 𑯅u<:ݯkAzuov8ffa&P5Tu,W%xA! v=wl5 Y-Ϫ\(ۚJuW:܀Hegs.v̸us3ڰ<{/=jJk$Cv/}u9:yTy(=XZgU.\mNu]P 9|+HKۇSl{1^GpdBHz%|9PsB5`[(9p DgU.\m-#*aX0|ut#Sgx~졏OU5e[O%B`:|k`6ދ޿Kep*a wzAbl$Ņ:6pOҢNڪ_|,pAw|v>W<#diewj`HhԌT=n)=<[ ` H Ϙ.܃jutb<< 8;GXlΰ|GbeE! wD%_|b0 =eu‘+Y8PxG]|?< 6W99-/\(Z#%@%uQ .kY2vV%! 9lA7"|W$=/\kpFjyVe/\mm : 𑯄u#^L"{RÕ]X9BGՅ<c$7g`"ΪټR6b䬖gUeA*aXWmۏKΑGk`茩|E#o5/gcy-sQ14/B@G46k6틽RJl >ka:QB*׿CgCgU.\mAOEB`:|+al09'%_Tp TWj֑x9cR39A䬖gU>\sQ[_ԂX5M篃;1Ǡw+aL|oL;QhX 1:'9i|V˳*G>e[B*!*A@XtG[F{M ;3p|쳌wPqض;ؿ4_g1D֎Dk Z@BFb)>)wg_x- /58]0ssK|egoZzr ! υM⇦o|V˳*skeU" wXGaFJX!CCHYfd"j|V˳*}.ʶVJ{Xb)>)+al}&ַ\7>e݂B:H=LѨ<]VXX0gy] ~mm~E:7S!;b~uhTu$[㤁knΆ5/Z|& W$69@n?b}.ʶv{/jA@ ӕb0BbpI w-g2 /r=.q8mҳZUsQ>" /֑#{Qy( ,]$)̕-Y}֗{G䰆:i ='cV˳*Ga>e[B*!*A@5 cF*YEH\86:="22Y}.ʶTBTXb)>)k`8a .:w9čW[c2(c_ x~jyVx#eE! wXGaFXYTs`B>nl7 65)Flk(-j@@ӕf@` |0.o#Y[% %:Y-Ϫ\(Z}H=LѨ<]  B#ZK5, [#vlk wXGaFJX_eٹ>9>KcXbv cV˳*}.ʶUBTXmVAXזJk`Jbs3I!rg5rBeyB|U*G@c{WXJ6l%#EF\]0i[߬_C[ V_rt2Ӳe[KTZԀXG PD"0L#Axt6X49YMiDTq:KLxn>ӭjFK|򄲭MB`[:{ūunM:.`IXΪv$ >^3[b.ʶ2!b{5:VF+a?U $&u̪Ok?鏿ḬFT;mV&7HNieyBU$vA@c׸Xf%__ӏ>A5-oz<cmkrc- e[YB`/:Bu:Ct;>^<L<|mN߿|1+g{ +4-a3eyBֺh\썀X=PՃg st!DjIewxt8x3Dsn}a5.dz/h~]kau򄲭ʨB:j鉥rr׾ʷE4o7V؝u'^)Mzdq+`V}|Ensze-w1ViỌkX,":4=@&25<074ڷ,O(ZzJ 먡Vp~c>Y}iާՒ:K/OAABc<d FO B VOjP -ԺXRw[ziZ}n=u>Qy23dFdf&'vd'W|Ί+du!epF|' !6NGS'ԕmg,z ;1abo DoM0"AG uE]B`:6I͹a)$hut[ nl8홁kYjNFO/$7L[9m{z]O!9b߂*>4,+UP=i!?fyKB9 W ϗl3g $9\L]PWZm# ֱ>KRA!_-.߰fe@l"}@Jt[&uþ{.3X$KؽZG``nb8{u?u½! b޼ +v@g0sXL͟e,#r> uE]B`:a߽y,W%^-!=vdBuЏȧRWZ%6A@ݛwr%Y_jIfo9605!9zf,w귇 :lz[C{rAm_}@JTA4XGܼ_B\`6nLbgt+}yj{ĉ\b0!+٢بkQoKS6ʺH]inkr77 3ǧͽ%^ܼFл Tcb^:4%<7 c1kׄ>T}@J Z{6u㾹y 2\Sj տg&_z]PBOp0 DDiWwn_PU/B@ͻ&b`ZZH)gsw}igVwA'~ }taxhQ2JR+̓hywD=T:mQsϯ/$ BrM9%clK3F+5cepY>?7 RWZ媕Xr77a]]⒑K3 2\Sjszs@z?;Lm94Qb^; y;%ԕv M(e,7~1e)3X6GovDi&cj4KynLxskssMHH]inkr1\Hw0A QKy08Sǥ^FX6 ־)c(RWZ>j#D@F;Or$ 9/Lol UxHF 8:zg+(` 92ҽёRRWZ Q{#BBdCȴ\߂Z뛙>ESLm >q^҆4rpn(q΍K63Rx4I@#u߈{UD$ FivW jɤׄUC`x4η8/,w")sC(sμtD> u Q5B`[:o_ED`fw5n,៰ŝ9}n 4vd\ Br 1 BvԒm`Nm \!.#+  yѻ5r^WSlXG-hǼ_˨QXlD-'-Y6>5yw65nXLغH]i~,Z@@.4b/$$Af0L%34w{Xd =8E fk> u͸v"E@F{$ i~sddgu؀0?%N )0d̗RWZ#A@ͻ]n܅EfƩ7WKb'ꗌ'揄NjoKdq}T;O>a!?}Ұ)> u {!-bon0x_4#@Qn: cF_2&Nٞ}\GDr.[+wD}@JuU?u߂F;%9Q+KP]?I 'f'4rơXa*5dtRq˿Sjyuߵ̻r8w=Cf  9WW.+E7Da /3=nVFm'bħ4Jt[&uނywݮe9]}cd \ډ '3$~f%kcJG6\ >D7״UH'|?WnHI-#F#OR> uu/Ze!bw!ӼGlxdWy9Vgc'7 ] `@eCH\ua~h\<DdBmx Ob-NnL'zW@~FuHKlXG99=+2ͻH7ݶE.\ǃE# 쮤Z2vA>>HkF43S-I"6?u;:;RWZ[lXGHwog2{*pѭiffdzC k&08C4}LGfԕ貪fӢ6i'NΩ4Ø5#ь+іdA\7F FivWR-uTc gh-u=#M 4l? O,lK ?8=\ ǑP LT-&9;.P[+-m Q{Ҽ簋P\q&e,oseG) G:F[f\I֗TKq @FUxCUGB B`P6f OcILF9E'G<1o\+-r%6A@yLncp'vr|^͵Mخw=Ld{ϤZQn餽aQHiDK3P6dqf9s1$W~3Rqrac> uůQ{a)=i#;+Ӽ\Ӽ/ ?ͱ}?\1̩!#3X!Ōx%븸Dy⚙ GX6`}|dV?O>ѯ {TU+-|M#A@ᑽ材{Ӱ~dr"h c쥀 eAf0N2~+oSnZ71bD K5=#B 8Ũ| na(w}@JV5B`[:4ٕ:w26uAc2Ւ@iC;>Ӽ=ۺr<{}ƶ$ϖ1r 7Vׁ17ߓw“Ÿ|@P~6z|kxr}@J˽Fk! QtҼ簋PLU؛weeg^gqn@^ymR$Ւgt`XѐNRD tyosXrlCP Ef؍J[B$S Q4ܕo1l6a ?-G-27 z(Nu_\ =ETK 7Bk4-n4}~,#'TprG$pay.&b 6RWڝhc{F;gҼm7uҺoCdGai2S-p4I4p|%C%gGC'/~vHaOlɦÀ9?P.* : ;iC<^46?vwsE$gC?EFp눀Ij [\!moR-  !A:LFp3g2!n"ydza:oxhwRlXG9{IIw؜D̻쐯F#Fcb(2$ݕTK\aTKe`c-Ljtx,g4iP BY=S\B!PXG9I޳ؓ~w*K{-IH0%פ 쮤Z^(p(jZ*Bqi^ECMj!IɟQuH]inkrؓ=b2ͻ j}pg_R;6Xf0Njc?FGg(gZ:p:g`1z>I1x>؋=,PKpȱ͡P+nOKlXG=HN7瘟#pqF3|*iAf0NjɐzXo+D3_-SP#{s14m-dO*n7`[K |GS+-k Q{ҼGwrWyǽ=ɶJpb7j"nH3j L$ԢI%bPgKG)=zn> u>$ĭr"osH]in(@ flgӉLċm{0U jA뜻/3q;$d8=}c.P5ji.8SS1iIF{y>,'D Q\-zKc:3* h4[x4I@#u߈Iks΅pSy~f?d"wj؅M`bWK͙ LHL?ޏCa`2 ŵy~Ņ7R|Fd}@J^ju4ܕo޻ɹ~0 6ťf iwOR-{ѤF6TKK#,VI흯Lr hL+K"0Q簱ԕ;h4ܕ4g֒rr8mg̚ Qhjr%Y_BR-Isѝ2,G3vߡZ]Mm2Meol\W(v/W.ԕ6:P,3!6i#;+igpn%䩖 uuhې4!ӝS?ϼGG/<j -26^TKc6OOfyjY1 Ob< u@_2HP4ǂHbV 4ԕv M(=i#;+߼c'_NMtec93a dش'+aYWX*'2"~JV\@4pʯԕ6ju4Iji:0z_99l΍ i:/YS6lB\#}ic|Nox…B!9ԕ6u45uؿri5;?~[kΖ@jiI>E3_-EI+Nog+,' ˌBm.O+-g (G>i#;+׼Dwz`h!C阿@Q"e˕d} It:?9,js]Ə/pn߼ۨ l!Ksj|5<4s$-'ՒQ؈"B]g]h,)3iR> uM5+ QrҼGwrWy'zV+sV!rhPBR-a]"ʙ"gΉfYZY:xJq9:D,XG9I];>۹ar%!Zrx#{Qjmt/_H@qL&.O+m҅Xr)xwertsƴސw:_1,Xf.+G!&f> /qY(1C\y[!WXrfPNRWZϪ[! Q|yUż4}D^pjJQR MIQK@bTѩmfܤWWndXlpl6uW uUD(2Ӽ%wߖ)d۩4fA NJ 2~B瓋 +2aAPbn}6yvgǩxl}ntrBJtj5+ G];8EyBYE .>5S9e^7Qx\#Ԙf206^uN=hYجV}Úαu:TQ4XGmHwcn:F7 3mU`SI8/}2JZZ\SuCʕ+ac{Q u] Qmlu".! b'ͻ'@,:b!l$'tiu,6a='Ԙz8J%>1B1i&IE>98Clȓ2=7soIcCMBkTr݌da*]0!ˡU5.@#.MT|L%Gf0N2k/a!_-["sagaYDŽ6ac Lȍ7ԕ" Ec^9i2¿WO1 6 94VSh}ǼKdKؤZy?NR_+c 0d#-]k<\%=n+N2b'{7Si ap}-g#5ej$|y.{S˕Qudb޲i\ݰ\Ea1 O` uE]B`EIO%ӏca}zK>Z7Dz\F6yQ2]4NTKa3$jy*p~ K'g7> u {!-b'ͻua"#?),+Ow]k:6 ,4 3X 5$p I\J˱XkQ^WQҽH]iU.PB@E:Lwsט@(W&2]ug&o6<u".! b';FpAx6*X6C#4߽׽֏4387^a%#3Rkmj9TZF~gO%,P&F!#> uE]B`:aOwHpaƍ莈m! mO`jѺj2b7B vl/^H]ih֜-9uHCJ㗬#cRDaĠ9Fz tuZ&ERjϯTNv!O+Nֈ ! Qlyg8Ԅfo9 dp?㒚26^^&eH 9L*9K_{C@&;!Dt0a$1Fl& B\N9,VKdi&@f0dIh]rWZ6X\չ]Ey@> uE]B`:aOw p7(B ;`׸Q'١`jѺ|lѾ1.[٭hcԕv M(=i2[յݟF|yT2VwA! 7u3oR32pݕTˈ%we;]aOJ}pk=nku":,wFn5̻WBꌢxuK@Z.Y?I-^L͡, bzy\cu$m; 30l.cEr9$Qn2mcˁÐxF(DNZ;QWZ%6A@Iѝ xs/>`fwMR.)u``[\+-y5+# QxҼ9,L3!/:Le?%b2 Bl,<pIA KZH-sk6i,<#lu\RGS uz!b'ͻly_ȼr5hMBR-#ZܵZ!܃| h=|@:Qd :QMX̻- CgLkdGf\I֗Tˈ%w-?#OLuEƑ^!XG6T9氄#F~!lY?w/ȉ"9jQԲ嗊z`'6=K):^" qINN 3/E,hv͟=yf|ujѺ%r- lx?uu{=B`:qOur_aNƍȧ2LB IjicazZbR<,uҚIuFʛ-|2Hw|\;rd!p+ą}/38BR-#Zܵc,/nF,nRWZ* @: oDysKFj Z9bs;s`zWO`{B!=d{؞(^|WZJNSݻ6"w[ԕB:BҼ2ћψ1a^H2@ָ'8R`v$2u]9j.4ўqi<z*N\uG!> uEU;u4K n2"luj,b `T  !r}R-"`߬H}5?O"RWZ%6A@yuߒ+XVh,7NǸsxkjѺ[ẗ́>y^IU=<7:xyDZH-Sw|,Uo--tin˽?/[ȡ^.9jf9jYrVeD_ hu|s:Z^MyyB8eW6că25?_]4N=T6bC^ϺH]i~,Z@@.L2LZa^s󎗃PD!Df0d&eQfw oCS6,9rH]i^!>b'07 uE]B`:aO%aK 6\(>a<-[j쑑PP!hFYLOb9׀a,)r> uE]B`:aOw@fܶ$|<'V^w2*$ղfSjyBYW-GFv5ȅ}@Jt[&Q|5|yXx A P붫L7c#҉%l#~oJlXG9I>->O3J˹Რ\ PBf$VҼ7F`ɖjѺLl}ԕV2%JTA@Ƥy_'FU,:x3"@ hFdcJeZDu{@#ԕ֬~cgXGO#8]=Ay c%l_sh̴2JZF.kǬKc/ vkzH]i[4:ǘ[mFI-ѻxIe{pϤZ&EYO(??ËIyH#7RWZ%6A@y5X=ʵXF ɽkс]u1`v$2B*:bSXGbw,iލu01_uR8R|)RE?=1$$\#LILRH.{g:,J(AώM}.źe$We+$UCPui>Rܵcu?{ fu".! b'; `Lcа`@8rq19r V&"bq=|WhYEf\I֗Tˈ%w\猶lOVHúH]iO!9b ǼX~s]Voމb'FXk ߿# 3]9jQ|lP|Ft0<ԞH]inkr1xQȩh6<\-]~msq`v4C89QFP똇[F{w|pqQ"A;B#jyBYW-9Iad(n:2bwdg.g5,u 1il;=ѸCGN?> u~ (w8ggw?sz_ (e 2pՈZ6OcusA(}@Jt[&uނy w'.Tl{h ߮q#3Ʀ=-e{fI~sAfҍW+mê" Q~ /dB.$"Ia!s-bzИԲ1HCT6 p{xB1ȘcoOzz(wVȴWW۵zpIAjSma9dMԚCbM:SrH1s..  t/]sxPjDlH`.T6 +Aqb7y Ɠ@QmA Q V3|u(Aw,[ϖ1:JvԲDaD V20(:U:\#`>3YlriȌkfeStXG>fzF ֑RM#hׁnSIpiɽbIlЈZ6LK6GM<:"G}@Jt[&uވy`]A<(uЈZz;Y 2qPss҆Ur1dS?8OU^u Wx2nrx#jɵ/tPGX!010 y|ogdg!8TXGE0%l(w2 W|t _XG1hD-7N)ֱ|FuF;ьt?:JFԲ8jCquk@#ݢ8pc+']X$i܈Z6FnuTQBrhļa+ݳh&Sh!']X$i܈Z6FnuTQBrh=d%sIW-1 F@QbsF@`Y7FJZc\4nA-bR7X澷=}D}<"> uE]B`:ao¼\C3v9b%4 "e^f1ͮJ;;^ۂy,a8rpw3w2pr[杝ax+?IV+-m Q{ 9GV/!&״4!k pP ' ԕ6ju{.3X$KؽZ.)za̟Uu|'U Q~#voe˕d} W˅ A(O\ӧWԕ~ (72JݫBs%?aPғFNW+-m Qͻ`/ajL?wAYXi\u=^!>b޼ +vB1۳4>mn}@J[%I똎Yݛw-?߻Wn.ťwKu<ԕ6ju{.3X$KؽZ.a8 Y;׿9]HKlXG972JݫB~ Lc ' qVvuҺTYXG]ؽy,W%^-|>ǯVWsC@޼u+v A}/~K R" > u;B`#:ʁ߽y,W%^-aqWW)Ʒ9]HKlXG972JݫB% RR,d mtu".! b޼ +vKAzsr5C-Ktu".! b޼ +v A߀f@<, Tc!%(7bJݫB[pW_y\ly0B!rH]inkrwoe˕d} W˅ l0)WNİKS+-k Qͻ`/ajĎy+$Egu%E> u{=B`:q߽y,W%^-9,Bg`[Eq-焀XG޽y(W%^-W\n{){.Q$KؽZ.)>?C;YI{DNW+-m Qͻ`/ajD2j,_}y',JuU{.3X$KؽZ.)í9{ssM RWZ%6A@ݛwr%Y_r!H1Eq -gXGM߽y(W%^-x&ֱ{uݛwr%Y_r!Ha.EX~&CE!PXG|wo:ʕd} W˅ u{Y_xhBhK" Q~wo:ʕd} W˅ o>cؖyH]ifB`5:ʡ޽y,W%^-Llw\SsG=/!=b`]f\I֗{\R:ױ+@@\ vo:ʕd} W˅ %í31JmM{uݛwr%Y_r!H~-8ƫ:B[b{.Q$KؽZ.)r,??b -gXGM߽y(W%^-u$2rH]inkrwoe˕d} W˅ ,NȧRWZ%6A@ݛwr%Y_rYH|lYswQqɯ=n()`;Zh;*whB(IuuXdťg77%J Z{6ul{YY7\ Lyw2CG uE]B`:6J$s@@c]"{w~[JSe!b-܅I}*}ߛtbk';!De!PXG z{u>Q!F@#Ml Avۓ #rL]D]inkM`}R":uqΖwXǼ[ɺ<¹ JIV'ԕv M'# 1_fPϒ.}N.O+-k ֱ  `ly8,Y CsX恩!&߰ABGUǶE@cL`ax͍񬖦&xց +(Lyց'Rf<}4$.O+-g  IQKaH d@5=bn1Y¾KЧ.O+-g  9; 2I`Y~7u̻Y'ԕ6aU y6!49Jc{]a+m5t"!XG&P4:zbݑ<@Љ@&b@LvE=X uN$2fb܋{"ֱݩJ[ Hd" ֑ T;:ڹ-DcS'ԕ:D@#v+9X;ٞuvkVA'udN3vE=X uN$2fb܋{"ֱݩJ[ Hd" ֑ T;~klO:V5uyB]i L:2jz bݔ<@Љ@&b@̖h玴}9.O+~ LXTγ=6.;|6[?d'9:&QLjoJK:)7^! <H4ԧfQ bpQXǩ)On WA!ü (P7XG7h=!&qyG{+֑H N}D qrwMyaQW*Qooа{bCLT#$W# 4b'w—:-ü T@8bߠal6hA`wT!bh '1D5B <0xO{:I# qrLGa9o<0+(P7XG7h=!&qyG{+֑H N}b'wUxbh a¢!A`wT9:rPRE@X2uxsyG^XG!:q:Au 1Q#0?;*ޓ^$Djpu8[o<0‹(P7XG7hؽ?#>&@d՛ %1?ujB`IX$X ֑RSmu0y6;BYc*bj/u$!jZ#Xhs:6S; 11똇:"ഹ ]kob{FhFw9|RF@c6t:P!l|ulgurZ@@0:H15B`b3@!B XGu4xSԥ]" ֱ۪mqv!0!G@#Ok{>} Z!?:wOuE# ֱ-Ip(A@=+FM`hcB`Q6yܰɋ^ mg N } &ֱU!f&fM(+!4..!X\:h uNqnul:H7/>݋?~w)3upb-agul:F)CAAʛ ֱ :hu|G|?oW'֑Z.XBJ9# 8g=^XլXʀt瀀XXGz.}9^uŮXX:6,bSSuTTXXGOXGz%qV[bbhԳuLEL# QR bbm>bmޗXYn]:uuiS"11XGuH%Pu4:>߿`AۏLjt$Y{9Hb[sFX+/͛9>=ܓ?3"ή8l{l܃%d`XvX^cdb[c2.NOMaJnSa%;NݫǞ%uXsqu:>{WsG8tOvMjlBxʱϏ7`(O> ؓc.E#5]}s8|cꚧ%==Ru Nwu:Xa"pG ^'5(O/OG;X{>8n8Hx^r?c99VV0vxHD[ZXy_2#@ *ŋO~_ƛ ߣ \"/#aa:'9 ۃQ?G@c}u# AH [H&56!Px?wd\p |'¤d޽Xsqu:P3d#3Mq8]̑rWi'o5 >8E懖4눀] ֱ:Y! ZGH'y^c2O^h;?i~]0jǺf{"֑Z-XǂJ" xa U0QI3= ;؆R|hY!ARA@cuB@qԺ%EzsM %wKʎ}'FT ؚٽ [! ֱ:hud.o8!"Uí2_Ai\Cћe,qoqXێP9hB%*mXF{F@֑dWI9 e=u"X?=W8؄[QL춪"F[ ywu:lY񦇓s) W?Qb:(s GVaQP:WMvtu:η)[gO{{{?{Os~ƈ[ea&WkOhꍢaG0^Y:փ[g::Z`x#%X^PnI`c mܦw =0΄jpP^3>eR]#uxT ywXG Fg_uRcHMjY5v-?AqX!<ߍR7 ] ֱ:Y! `Y60K7B31rȼa?"|t(%l|ғgiC8 v!X j|ؖuJ}}=qO&di'![|XK =jۍa5b]4TM`IXǶtC)5&5q=zq[}pE Y5 ܳ1 |#ϯs\Ųpt Pb[!mYkQ(A<Ƥ^iqS0i &>ϏV} Z^\g̗L.;$^XB ywul:jfv gk4<\^'^ZEX[ Ea/,Xdj1QS!ѻ/>bc3X]:CXL9 ޤCs &:"SVi'Hc̾[UKj<b!PjuNt>u:7Vxg o%9=b !+H|H䧋눮tó'2I ] ֱ:Y! `)Ya=9W..9xFt =}&:Cc6@ ~R7:"Hj:urVu4:zZhL8\ž{>{LK˿A!aI!1 *D@cMu3A@Eqsb<2?gd *mcq1ؤ5uAXdhu"hLS^9'5>uK܂ke/[` NC{s=qu1Lw`Mo/?:fe:V\;:e68̑{BWpIK $  QD_ ęГд)c/'kr:<*lXV;F@#:;?~эƬRV\yk97q2I%9ܲR E ٗ]\w^*Pa+:B^1bIPmrP:'Xr0^7]e sX8I3(gpj6"ruԜk:<*lXV;F@#u"~p%cTqݣ23y>-9Sh{s=1zHbrM:D[::&{S7p!mmY΁;w YPP}@>A#! ԋuDѮuXgHHu;XX& 9nZXkdXLט3ѫx=KSbTNR~X댻G@#:< Fn]|^!'+ҘjMOkXxxxp0MGe_/^K{?ϗJbe uTQ"]:2Yd_~R\+[7?*pK~ϯj*A/6p0rW\"̑,a!TbucOu$YLJ}R=eY=9qaYLb#OWV MK0ׁMb$9x 9St1D5+# ֱ2:9 ֑d̜W$ 4$ 6ӌ^!u5x W@H&5 ԻS?{Lg Z }>0*, FOI::<zdgԸwU6sDtoR㈜]tu7z-6VQ܆ZR{ӠXGIZupY $x萿ӌ^a0+lW F|Z2D2~…rXG&Pjba+gXGu%dBzd:pƈf4f6[@vʧ^{mOrgIME@cY|%,aHd&0+{i㐣ȊdvcC7*(@!,gvOC#$5YeD@cٰIG6IM\d:\'/.#2mĞUG1h ߓXTԾ:b!@! ֑:‹M%!2<YLi mnQ уI`A#ĊԋKH* \f0+7غ!nJTuDѮuXgHƐl.f`?Y i(bD;7EgcO>sn܃664+n.R)GA ' O:HD[Z$wnrwx1Z:/pA|OiLI-BPCDkxuLb8=x#|l|ց1lO- dərc1ӄ]\Gߓ=b㸨vE:V[::ޭ;+C~Ԛ9gL!#-ȧ=x𰷷mBAz{?ɟDs8FcIծXNJ`T炀XGu  ΍^acT0zktװr1i^fÄfђك )B\mPW0p2n0v-=H>wRcUXǚh\gXG&2 HGwW05J%Gz^KniC=ej̄M p$ C{"UXڈ|gX aR]-:k”xm[In\"ݱK^OhEX=l,xz$QׇXGg7A@cu}# ֑:yRG#F^, "aPƒL퍢C= >CP,ڤ~Bi쌴!pbP9hc눀] ֱ:Y! ֑:A?jEj]Pcۄs)>Af> QPr&&9ُ2=9!^Pb[!$`$H9!C }P%r#> 8 ߐJsu4 gWil*2Fa"0eZ>D@cMu3A@#:KܻOgP7;a647|ȩ,C"Mh!L Iv?Fgr>u䠤6" ֱ(~u$Y9`|{ :t 嬃 8N$8\6l^$3=$T)BF! ֱ: ֑:FGR갎.~\'JAQo?#v\ã==Z^Fw +]nn m#F{VB@c%usB@#:\zH oXV̛ n]G_DŽ:4[^e'S_Ió4pϯ8yJԞ cB{l',z\\qY% >bc3$TQ2jA DwȆ:h8Ó aHX'o#8jU]{JJݽVqa-" zQ^AÔl&/V}!X j|H^$2##}qt9:SKmuXsnNHCFwue%8oKMqq(X(= M5{G$_G>bugu$bLL(9mZ}i&c#,ejӸHBcROpS짍݄ #:!΄!uxU ywXGuAD/зBU1؜ \`먨ae'/Av ؈&kt؆-utPy:6]'7bIuO#l10aq-%K9I͆{=z6F,6X 2<Pb[!$렁[ӌ^a*`JHΔRFOk)q $ƴPqgf.u$WXa?C:.A8EtX<'t*woI|x ŴRU(J{?2zd0E%ޓIR7lXXGZupY IDױ_*Lb`聍?*e/W}c{<7׽ЇtPO@x$Ƥƈ[I:hJuNsNu:0n=~沸&4$2=5xC N~ k<רpr\!sv˷r-E[:UpKJfŷD[Z$`[:RasXBUX[ؒ_C] +c5C4Hd7'p5ud5[尕E@#:Mj3H?:y.p/r,Fm N* s!TqA^':?']XG9A@cu]# ֑d3 ,u#(XaEǽWhC3a\r!36|CcUf D pk#ZupY $Tܤj/;% oӽϡBɭB[9^ݩ}K>I8Op 'T\z?(1D5+#*_ʘtG@#:|2aH * "8ZScT]-™9{\-rk c5uDvX:8,gXGu7A(ĉmxٵ eux0& wG|6ͤk\k" ֱ&:י 1udׁY *fV 6˰QE6p\kS [! ֱ:Hb'l^D H.ay`7luID_UYW֩ryeX[7#Fb0TmpYwXGu zy9 L"D6|}~4)*+ ,i)qQ.Y+IHEL$j4bK#,gXG&0.nx=X +ӌ^a8e MnwA\R8⻠`KM+"/K+袡&ulNo:7a+)#g>!#7/W XTArH$ |=ط98ۤNbIֱ 2FNұEwq_rfa(Fȏs:,ϯHb/XJ# ֑d4MZ΍^!ׁV8$-gf.'y1̻1a"ݶAFȧ7+X#vX:8,gXGu%|R1,(?'^ܼmsqѤSUtQpi6RD눀] ֱ:Y! ֑dnnR+Lb=~lx/[[t w눣+ ֱ:Ź! ֑d+uuTGZp97<{\>CZބ>d3'>\mSh`!&Y\q T ܂.LrDŽ>s:..9˹ |:z=:Zg::2Y-=ɠ l{iV:le1`z  ˁ\D4,]W|r=zy.P⠟1 /6M:\#WC@c5uA@#u],:.aCo+ ztz"0 ړ#zJKGʰP' Nxd>,&2^\ALl :@]9bc9_i,mF1ІMr9{u1a=}L\/ko g‰9I똎XGe@%N|iuh\|xp@9 [ǜ 9ѻQKI{9 87o؍K~:06ڳb+Ӝudaq^ݼs/yg{@=Aa];߹D|Y8rŏ:l 1+;XGE%jbpQB XGu aQ`qXBaL)X]O>˹\R&v**S3uL8 c]e >bc3$҉^ S?ҫ:+ >X;H ax8r>_&ʹ\&E|Vit[%qnXfE@u a6kd#:b\4SsOlSycngXrUXڈ|gXG9(uൎW0{)S8%B4ģbfT)cD""ݶ{I3"QJl:fC@rցW;cł6y"[$7$mL,Ϟbc3FXc|\sCFtccPÍk+kHmgnMH :BȨ~5:VZ'::Xǃ .Pׁ16U2u a*3GBcTV4gˤRvTuujJ:5" ֑:3X$BLGTA:\B`v+''k|5KLKnPPO.G!d[fwLc6t:b!a [^K17r븹FA( {+j<܃Uf\OkcOқ yY"KiM>*jRcUXǚh\gXG`Uc6_e8wx2EH~nI<^pGNg׬# ɢf*>}n>aXt:UR$I GC*|dg!ax d }cuqtxSn҅nnX&F@#:xrS|0wvMa90bNawѤ԰e1֑ }` ӌױosW'qI<-: ^bp5|=!d}˿vwي'^0nM͵$u^8by8Ղu,D+bIa[.lhB0} V'8xDVjnes%I$֑Z.XBJ9# ֑d6[v&|oceQ%\.  ^Yx*a6j$_\Ӆ?ŌEU" ֱ.:Y ֑d7}:kL! Xz1a+@rRxw{l=\(JFbE`!0XGu0¿L8Ҍ+1Yg DG>[d+f 95a)8;?~{Ω3"^_-PKc}鐺uS҄u$Y9(cq2Jj5CѭayB8(AqQiЌp ^l,uhv |d{0 Qu$YR[^.3Yhcg:XX͝Wi%+.ҒDNv:RaݵnXMKq:: c"Fq"`AeZ먅F@c6t:PH0.``RdIą78 !Lmx~<~g3uBRrf# 1:(Bu$Ya Ŏu^0 @*^_< D[זbqab) q wI}<1:2Y|F3]HsNAe D}OU_@Qh}u|wԷE@#uR u2rit˓XAL1A!ÉL}w%'vjYKr}'$vhʛ ֱ :d61e$֑fLMNZ# qPWbIAЦcs=cxd:&-^osX^A' XGsϕ: :vqum! ֑dL~H-$/^=Ǧ"L'Jb;VdB uЌ"AFtB>HfsX>~Y.z* ZuBRrG@#:X0NvF^<d'yroM$΄kх(b'zH^:|IO3zLqTTϖuhu,N@@cXj*H Ertp$qLlO> yhl]C+=~u( g q:BI[T$ֱ":8b㸨V ֑dP R}o\ct` LbDݒwuu.5u T[D@#:ȧ "RI2yǾ|M]XGSCb9rarٓgHf?t|Cr`Bc6t:b!HR\x|G۶rw̃៰q:γ qhv3@@#: v\x!9Y͓Iqoo8X݇; ZuBRrGw-\U'+!˅Fe>0Yʍ.u#ЕM `A $⠉ FۅE̐(Ӟg eի[uλQ:4/O_~<~!XƏ!:df`cZs6gqdJeg;za51':*XGLnt\]SOF؈aY5<ֱ*^CuC>":JXdzfR,]GXGtouv^X6':*XGa*x*HG|2'XGz Q:⛍I,p~Hw@ YCgXGGGx]*K ρ#:X.~Kn}~,n|ut7ٜ_XL "O~?9Q;!hY*X5:C˾Eu񔳟Ϋ_p\+gqitmu|[ߎfwB$ح?tKK Z+:ֱYMuʎKuE^+:ڏc%1Xǘy/kQ*(bq-y+.u :gur!n" K7յ#c =6!:` "sQ,!#:F̺>LuTm6Vd4GYXGy`Q`q}4l`)؝ou]f'u!m <@`@`52TTct6Xئ枅u%fXGsDuuPQҭ`cJ{1`͑j:jQ3/CE:JnCum*mYX\boNu4GAX9 XGͼ *: :gasٿ9`(`52TTct6Xئ枅u%fXGsDuuPQҭ`cJ{1`͑j:jQ3/CE:JnCum*mYX\boNu4GAX9 XGͼ *: :gasٿ9`(`52TTct6Xئ枅u%fXGsDuuPQҭ`cJ{1`͑j:jQ3/CE:JnCu^7^{4^'f8 7`79'`'kx`Lnjؠ@`9ֱ8 :|wSN pq#p2q2ߍW5 qd_Jbc;#DuDLuՁqdqe6 qd㷟_}[#~?}n$:nD88\>at]Gl6j`jCȶQ!;{;Hu܈? p*q*MsZ3|V|6hZ XP`D6|O:.//mdd n$:nD88\㦿DCw1AӚ@Xb@8$1{..<FuFǿ!pq9⻎J؎  N XFu,h^@u&> Nup9G|M2$Au?#0Ϭ)3[>h ͘{dO<':K!efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k efKO el!%XzMMٚD&26YGEw#Rf^ R&_"\Bu,ؔIAo)cu]Tz:k`{݋^ PG?|+{YulI۹':g~p˘ټ@&1[ֱ*^`):'N`)ul 6&:6t -X-pSXGI&:BX|`)؝t3c3NIu܉gMuu`%Tw "XG"|ހS G`Gۙ`g8a#@XGiթ=ֱGaH':S U UjEue#:.eF kAu(@Q'"YX6@Ӹ9\sɔ8@`c,G1r`2"XG[ZC`K9>Q?G"Dqqr=fOYǘykIűZ`HjXNu,gXG F#:Fhee\@2Q9;b[Nu,g@ъvj`5"*3uk=EXGp ֱc@XG[ZFuTˈx@`dcw1B΅8L44nB :֠:XG\`joq^\c.1#`rQoK/@"*A                               \( endstream endobj 90 0 obj 109654 endobj 92 0 obj << /Length 93 0 R /Filter /FlateDecode >> stream xWMs0+5`v&94mfzLCCP$$d+! 20l}ڷ|a@dq 1/rW얗~X.>26 Bs So)đO.E3,Oi_PUC6; - 8,&x 6xvpQ$е"> `-D]R摸@SdQOUXDBiƺhjgV\hWз<1tKyn{ r>8ϸ2Q:ф[F;15Y lG]/5Om 0e$M7jK8-lSÅ+E=*9,(|Ԩ%2&FK܋ajҡsTLU71r Y.$h0VN0ZaC! Z7شC{GJDU%J˼:#{&6%tio6GFftSMw|-|(dEO4z+tؐiIܒQ6'.j xEۗL;V/ ^s8Kb:'!+ /Z~w0<"cЀHm${>ʃjYl&|J(JpgٞچQnrjin*?~2 endstream endobj 93 0 obj 743 endobj 91 0 obj << /Type /Page /Parent 85 0 R /Resources 94 0 R /Contents 92 0 R /MediaBox [0 0 612 792] >> endobj 94 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 96 0 obj << /Length 97 0 R /Filter /FlateDecode >> stream x+TT(T0P053P076W03W(JUWSH-JN-()MQ(+)0S50553JU5Sp| endstream endobj 97 0 obj 88 endobj 95 0 obj << /Type /Page /Parent 85 0 R /Resources 98 0 R /Contents 96 0 R /MediaBox [0 0 792 612] >> endobj 98 0 obj << /ProcSet [ /PDF /ImageB /ImageC /ImageI ] /XObject << /Im6 99 0 R >> >> endobj 99 0 obj << /Length 100 0 R /Type /XObject /Subtype /Image /Width 941 /Height 1391 /ColorSpace 7 0 R /Interpolate true /BitsPerComponent 8 /Filter /FlateDecode >> stream xM%Mr׭1/6 c-2JaQ B/Ix' |h..Ɣ Mhpx@ӔGY{2TsN4 2#"_⟑Qy2B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4 B@h 4p~~7W=2C@h 4 GQ}:]_+'@h 4 BOUϺ`/hx54 Bk`\|-B@h 4 \qapx/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx54 Bk`PqC@h 4 Bi`P5{ B@h50. C@h 4 .08b/hx / Bc5z9=<|wGB|?2@h 4pY8 #6.%pV0[{$J Bw?0ʣi`\|ά![h 4p z qapށ$1QA#4~*|>xD}BB/]_\姪pPw LLw!bh 4p zqa!D9b>545~:Dž|^xD}"BW@̮WPGl]]4s bv}qAapށ01QA#4~*Dž'jmB@̮yGlyzH4r \A1^A'rP'w!JLw!dh 4U~ |^xD}"BW@̮WPBk &qH \C1^Cs\w ILw!bh 4p zAapޅ(1QE3k fW*Glz4r \A1^A'r\|FQί!ah 4p z qapށ$1QA#4~*|>xD}BB/]_\姪pPw LLw!bh 4p zqa!D9b>545~:Dž|^xD}"BW@̮WPGl]]4s bv}q~_ѷ`w LLw!bh 4p z+`<?mE5Br~ D}6 Ckh fkh sOCC@h5Kk\][K¿ͯ"6\D}6 Ckh fkhW7<41QC@hZ?OW ,_i#X&|.xD}RB/]_Z`DŽ~N OVc~-'b /MLo04bvS-`^s,`LY"!IL!eh 45~n! %HlsCHsr D}B+i f+)$ @NNOoy7 >I1bAC@̮/Uv 0s,0`/7s;KGlz BW@̮WRI %twfwv#&i44xQ >]e9Cik̭BӀfW|ec!jXu.΋g6da0CءN_h` 1JY3&f$ %I/o818W"6SE4p쌠 :S?b@?w,`vr{k1_ָgι)Z\o w!-h;V0?2]Q:r Z+s;xk[3  ޡx%4pٸyK "}#sވ:ѯ*a0Wqn05#px;(wRl5> "/@?J|U\~P ?o_Yځ|>U@JWBk08+7Q:獨Zw<wQl'  (64km}7E^~bZ x?o}w}|EEաV 3 ֠nXЇ@?w{VHol~}:JsVvYN?뒜0Vaf d0?SgYd4Zgk~nމ w ڿ/^qmec%sx[%Jlo>>߼"[&D _#ݖ[Hݗ$LJEW^OoO}** gz[͆@@ZOdzhkFt6Sft 8|/gOn[b=t" 0X'Ep{g"64=Ngwo׿1fGX1nHgZNHp?)&YEMl#Ϧ_}O3}+_R/*⁚v7EQGVltH7 a!(9UV>oY͟2#ON#^4l . -v{s_WggOgpni/xrP_9Ȉ]o [;XLqvgc;4֡?Z&ʛ&Իx䌭J&3/0Oo0KRwt" 0s5fzމ eDdF,[\$2#2%ݝQB8v`|v-mi,]ɶ蛈DᶩpQmǭ uNX;*GE P2L|l"*RZf?gU s*+&gxV65u6bV|JfVέiѮIe9QV\fV9F8bOJlO'͜ul_.l.yS`H0"&z)qk>fJGfSJo߳׬Sח~6nY%9ap+o AM,g6˯ok }>-o\oj;)y'XmS(rDV]4̥.nI*ZJ+|& F62sF&mj@+>2 pH񫏤*e_WO~k eڞ$A}MNGMl#QoB}m״;4l Kkٸӽap}H`SN}é}yQ;̷K --$FE?mm8:OL9tNG< V,,^)l:u(-'{w%`q3<4`ke6*[ߛ[Pv%#c*_~pZ]8I&@g7 2@*UU@pY'd0ɔpЪLѳ{zeۏ956"eVX[>K`mlj| Z5b `۔C;cx?nDdsPߡ^u8@y]{T2oiRb›u9lKFzblET"GLBeoPL[ƭq/7 ކfSS[ 4Y1JlP/kJՠ#X֖C.Z޻n۰3`(hA_z-c* B"!*Cz(NrI, ޳. n2Tx.?~0T8 b pE[GLH@Πfkv?HQo\Pl.7/ Պjj&?&fi7@cjr^IO?~`Yw{s:"QVd~B90Lrۤ-Vm"@ 5 @bemC(N~Z#wM@qfYᵦ/Y%nxm^o".VdZᢩdpb lzI҈S يJ&9vQ 66jFuqKY. TjQ:/OB= Ƥy@8 0Ă}e#6xwo& &gƚ)<r*aBYc8;MO-ҷ>?>RS]SMEY&aS351dAj}"mqEwgc:E+$"psS[4[xд;=- t,32Պ/M?R{TL9̟&_+`yq˂%§idt ~6ѯqaĆ{q :iJ5[֒*;!ͻ5Tx#ڣzdl%nwE#ŚINE?G4[h_~oReȣs4dx@bal#d[>gޠL&TE3JTנdZ}F)^k.)&Alߧn8 /cRxEZg1Ʃ9ڞ,>j3(WJb-Ebl*ة~V_sf\@hO 3{8Vmk H~o숙rG+6Eq}g\6G yk6Θ8'zqΙytVd3',$Q7,/ÓPz⑸pn#:1\fX锉U bqQ8 rw]IVvnMJzz`}פ@{\aiKH>}l\ݙap%:K|5Պ]Z;g+23[m9kd_+t8A=n `[{Ÿ_2QA," Yp5bίL3%,)2+$lv2W3KyIF4f A-~*$o"Mjd{pl\ݙan/.p,J.K+`jMP(sE1' G[ᓴS1n RqJ $Z?KPn1U=b<o9%(;ռw])D9y4Voq:7HiҞ7 5‰}#OD\),sz8Q:䳌~6j. NԭgL[%֖-Fp>}&'x`iCF g/nG霹\Kk0|mX7 ۙع`K.rƕ0 B]eʹmw>jEbm!{ڌ:|>bOJD.9uB2ap~#^J6s#tN9Mm19Ч0cQm?gU kԗq4.=Ή& kKֆ^}0-?4Le D^s.%=SNV +׶gje\ , G@ ,`0×`!́NngiVpF5/ff%s+F)7D L7ϔK3YŸ*P0_sXx(NTͣ+*s s$#8'>Br>I>\m"Bw`JjcE ?q. ȰYH[Z;g%/T)ジC 82*AȺE1ԕ9S&Bo|Ϊ[`NJ] m8W= GҲ$48ReOg JHxB8^n on"kI#O>M1eTrQX|t>\m"BP.W51lzH7 >Iz!hjJ?Xfs 4sҪ}YG3Nvۯz`:"9nsD]&q) ^ڗHu$Z̴b]j_@Ee/~Vjgo~IRN̈WeT)3f˚f%!|U1fAL_UyXlUqH7 dVT`}͋l9wwg_%sթQ:*9]V #6l2 w1/"ֶsMHT&jG'-`'bf0S ħu %ج 4HzslJ\vKlb^Oos*" K-'mO%ڝ%T-q}qK. _W|7Li"2NxοV/-7~zz= Daymd2H%;fGUح[ F?k so@XLJ = V~b#ad{84y6X}2:pa[k@@y|@4 VXV7ij~6nՅa9kI}1!VNm*dLJZJY[H 7ﴎE_K9?G~V th4p0$Ll0x3c;|r5[)Ke1D%OVN= )f;{Nǻ$NVd!-I2ê uDhצ )ʕgvRo[L]w7>t@O֮Phm|D͢OAQ:R3k08b A1"dDl&?ۦ[lfW)}q×8K?˻ѧ*S`lHODk5+\)#H6y:Lr+rRQW5]44B,^Mٸ }ѳ= 7zŶ% @z#Ї5n=J;O% C BJ>㖙@ @Q|&/_m;)FdR簦d8L"u%jb8ta[9f2'vht`4NQr"htR09YQ5B?81oo{,14G+R7Tl,GiIz'C?J4U2ѯ(1>[O4!ԙ4p fR s5Јp9Nh& CJH(5WĢԑ9O08s֔L k@ 'sVxMTOUab3u[VH !\iXv__+[x B5p :^Wss>1/r 1Oq jbf@TZ>e~qpQ2r|$&+n"ph>NvsVk23J-9k 1! aZ h2eiqz bKt?뒜0GO`YƔUZpLzAq=g-nixDV;`0c]lj\  V).ܚ3TT3Yʫ m30CsxݿltV2<̈́KY ĝxu?QSOtmIjW&wdS}_Y{Nv'r*i-~6Ϊ8$L/i:Q%󴆪0i*ʟ.5ڌIfιTrBvh>SÅ,citd f ;50fZZ.A=CB?^kjĴ1X$:)M$tf g`m&ot*2%Ηz4S`'R9``Thm&[a5SqM؞΋60LS.Kۮ7 ᥩP`kA4sEyt+?a`̣tzuHٯ6cd7[%x ljH@ ꩄ'7EYBlzazrf&bmL VƬc͜AخT %`s + W F:{Ax?{U9`mNo=Rgc%،Ve N @|fs~̑(4Я6cd71gmhN46gq cY\2 J*d.وYZgVP89`|DI?♼KWGRG0y MlbW?oB#]د,\?~HVzMEq rٸ}uuopkAV^| g#A%rZYj[JEkՁ__+PcF/|Ti^!Qhίi6!; r`V@85f7]p^$Z&&N;=BcivM \?VmT \{`SRԓ{n{]SK}~?WC eoOF+BUvg4pŒ^08m=w{J?nv%D'8&?JiVa+M0dmV+S<ן}$MΌ E@2J4O Vرk)[- lj~ʃ#uȅu<SC*9ibL1YNX}~l[m0NM +x37Y%߾56G霛 MЯꭰcm:n`>4C=G`0S e~$B\ ncDB}W,JO~k@_~@EYYJgN],|rX8!48'N8懽a nVrf ]ю.aKWjۓ=J#8kz+495]O /eNį/UƵ] :"KNQ]Z<,TB?0kk%`m31)L?̊5NGk#_+[M$h"4C^Z;˩)g`>Fzz&3Us0[uZ_xڃkQ;*%4E_ryN?ry7 .x ZZb~֡TĻص[G霭rE߯*֦Vx,=@L?ӭ3$+a06D#渟p$bUIMĪ%P:-CuZx;ܩpɨ,X4٣x$/~+Fn`R9kpբVck~?=kz+X۵GV[6ds 4 @pX^1b1:\e01j8?ΛP߾w8ibJE ː9ѩI2k&J$JL7:@ShT(Pqk˿7X rƖ4).?gpʧiܼk+Īaqy-{;5[2lb=b@,-@btVƢ MVhjz+n/K`FlM(ka CFoԵdUMn/Lp '>)0 " V[ bZ8]^d^ GUOyۈ5e_fEF?0Qol^Zk)V)SA+}SN#TOڗ9N_+[ᥑa :".@u0{`^_nd'`6Lh>**S9 %|&F[ڱ3UWJh+b RBhCߦXƶ)pVHQd̀zlq]{kP" v^˟Vf|,FI֩V,1+?,DEBs e+}j.k-`h- O+V1aG霹Kkz+49ZcHi~&Oǃ-iA_p;xp& DM~)GJ:~42•L,9YX1Hn98'iV n!SejvS1B0uH Rj*d F?&ξ|_5I,*CKy… t6*B~gkom@6 Ql])87`k_kJA0w- 1d6ٸ%o0g/HzN?X%fD5n6R[ p&6nV?[`;S1KLM?Q:# kz+\~&cB> tVus)X%/[°n9Szj.΋Ok紞(eXBY @bn? ' ~!Q|~6ί& 0K@/{ZL0`+uYJ[NoRHCL.DeC8~i 2ѯ(ه< )C@ҁ N*  P"Hp&b*4ƉBW|s bee)vQe,zM~6έa= ғi$2-fTvje?֚ _6s֖J[``UhFuz_+£j< z@4̄铱i=*^tVDvD{ E#%&3Xw/pS4&˸*$c'±gK[d_'^ F?jǣ }*@|#9l+&V@uVi X3o9wmWu@MzhOs7}W/5p#CvZ=\:>0'NL GT-J4)|A_P,l(K6~{20O{O S5[pK`0.6D'uE>SMFϺN,X}~6ΫY7XC K+A!\k1[i)L,8Jn夵'ku%КJF+t" ?6ZǦ+׶i$4ЪF Rb>;UqeDUK5rD 1!>[bdANw٬Zֲ b"qo$`"1߾ˈr8]"eS, S~NGMKXT'Al@;LM.VMW`84@ M#9!Lo{GUʉz24I BFkq0G)l;]-B4Yb&RTO$HJ>>3O^۹KqxJC GCE"'T+];MH"v+2يR4ч38[3=%'hBmkqVm'*@J-&.raL&m /mU`mZ$N|:?D"G !%[ӭ3[VNW$jd^PrQ*]>CR .Vgt'ed'Ĺe I4?{"x1ٷj*`5,Z@p ~2OуHxi |.<0WP& 2Y16+pY6<PǙclV|=ܒ~r͹]ɭ4)Y%'1dɚaq.?S: 1#[TAZS^X`OУ4SZ!K<@礐&z#SYq0O"3ULi(M2~F9K ]Jk(4O?s[ *!-]n$W"v}yʅ{0V4E߾W%$kKztSh\kļ2ao0tɰ~4@>q#'@b*ĭ 8Z>Eu8:rV(V(6(d㷘yC$ݺ !x?$膸Jn⤩'?a`^#:ǹeks?M栭0(ۧ6!k1GTZU  E[GuM.,CSï):%64PRy_SJq}wlU_ O yb@2 bK52^m夲XЂ,CV@2fcQ V´ѯe2`ftn݊ ͆k*zr7i yW<0MIe@V (>ebVm)[$ QwYN= Fl V)CO6 S+:ijGFOQ<ld =x+*`U1EYe:ЯU󨕓2ѶKě6,d0dBY^_x7:ݴO?i8h4F*Kny&glf E- Φ-xes9j9SΠ0(g9!ˍk1G[Ul ;r{ћc|qWw2Px݊؆gM)z.# r>Ռ(ƢGxL=SmL?=~|_ilm)˟gf{;J9(p]:h H׬0)|Wz]@qcT,svkx@~4G6薌\@_%XK5NvhUT&69,gI֯Ql\% +85K,00 n-ݵt覐Vz^ck^?J\[.aV[ 4-8C5,^YQVl%?E㆖Ęeou ɚGʦ_}Z,red0]6(Հ[׷xl( ;pOF2QIeQ3~%S(Ƣ c+Z}+_4p>{4y^l3I)1iL8ھы9f `;7۩[cd]gOw>qJ ))n?hT;Rgd \N[Q`R O1/y(Ƣ365HTafahG=|tk_Ybl^ =J|U\A[u^QuhR%|][ClraqZ3aF3Ki >}AxvT>r'qTQOU`:4ؖ0nf=ǔӣ J9ҷ3LF<[ l+h1bu-A.XO<"'C@Ĭ&m 2yV=L"IpQh2.rO n(Ƣkk;ڐك|o<=LS^z_3t=ZA[{//F)t"qBJ~2pkMΰG^y`vf4}\¾`q=t 8u&ԋ*$hhu-Ys.9?SΠ]ۙ8dO 0F#|Le Jou0;^LC%MnզEchS!9D"$ 1$;ɸye`@oWzjk ZH=X+ G9i:vt)01XH-+d(mA>A Fc<^ps9WZ0~q,S_.RĻsO|h8m EUp;d9|& D5{IY-cݎb,|jDD)aJ,oÀjiLtX7邃h8H?0ybŖL9)gVmwB 0F;{D1UQ&(JgU|>D0`Ml*߼M-\x(}Y{Nqy-cGq> 1t庵eOUsbvk3z(N$`駍W|NF霾?e{ bJh2_'UL`qtG\­dK@bHR/V>5No<΢RF100`l:փq'opF@6"C~TϠfm[Q:g K M;_sD4qp=/l% +#%a0rP8S`ǔu5dCUpXESE!&GQ2AvX _K8TG#a$I -ҳ2Fq> YP R[Zbk1_P-0=c5aß޵TNvf$s sY-k/9?G0(3!`̰ڭ,)xXaF0[fTFa.P)pۇU*37-Ÿ_XN'½yfU0!d|s I̧޼gz!g(‡hڄEl&0g'2 H6}->lN!k@x9Ojy4J頭mG3`Y~#"@ܒ, @DJEkw_R5 N(O` &)/ApS4bZ˗;brl^%o~!:tN2:E=0GOk_2"K=q19[9n,Hi5ieiEebVSv65ɁYCŜr R:dp $6yt ?e/}mPh3W.+3OsekIs_]ٟv*jF10$F#aս+y,x`qf/߼cJ<#֪\Dګ ІR9q>J4O` B'T7#ok`u+bGF>ǝGo*N!Q)[ ` bկKNtP?H'v:~mN/--$D~kbse4f*pwh gnrE dظJLgnqij>ӎIi[U!E;y|vsM$c9S&mA>e nSi su^c)r{G]iq5N9G)pwx#s,yYK|0as[&IIRRFLV\4_ǝCJlcQ bίMl .T[DҦZdKt{%Zᦒ'0gmK1A[m5z`88v_L.f{~xna0PAfMdu؝5w1)N-B\?^,?rsUy%J^+|M<Um S~[5;d)6U`'0˧M6-uAML͉Lk \w2Q:/OmAi12îk4~/]'Uc";ZdvJ֭uɬP<Uj>/a#=jvQE% 8?ĺNlcCeΆOl,je?lܬ-2eq/[3}9)gV>v YBM`88! qꯏam/<0 `quGZeG0f+-:i㤤,4}@`Z߄"w#ظJ J8G׭b>:0?uת#b2&Fv(.|'LڡI1εZs.%=SΠ0(g9!ˍk1bXOB14aG8\)~ [0O;~OϵPI ܝ֩IFioȤB2Ab$HpC|NJL<𓺦zKo`GE7(Bl % }O2 F%K {r6QlF):Nhm 2J\L9lLmvh1_{ޫ|zℙɹ`8sՐI,<(;>'g:n>Jk_QStBh#`pB&HO$EjW Y1Ftd7hu9`jNȨWCE%3ѭY'E}b-)'d~]9xĖC `?>?yÈI9sϗe|'$Y 0Ffչp9)NX uawll3pSFTb~7'V!`JK`Kpn[&.a`cQ S\넁q>&#RViɮu1tNA[gm+4p#>I{r^os1fS?*`CjD2BJs"JiJ0]}Xlr\)֧tSۭŎb*a0 MnH۪c.K0i03UAڼ1kp~Y XF'\u:J4Oe](M 0F#dR~*Zc`4-J2HN7H"8Q5U+mcՇϭ=mbުI4XS`N3:%w2~$s/m [G&O|@E}aewn+wpiD@SvS;dl*Ql\%  }Osl۪~6k3NL$l %KR4WXE( 1:tnϿTgPQDDв'MZn*FƦsQE% y+o7MgE" &baf5cagY:[>3=oG9K L!QhUv-`]ߘ'WPHJ )H1?O[_ז;=O.Ho;%AԚ!Q} 'V=E&yG2Jsٻm~ Ql\o|4#ckӅ^)B՟N/gI">y`Tռ<sRtV{K?#=׌&9vQXcnA9XX o^M;˅kd_O_ 4%Ϯ(e2!U]XYԒlc/5sǔq7!L X~ֈOYE:-ۯͯeQ˜Q:3 Ϝgj%4p#9p-Ite`OM M@$hԔIn-@x'06pB\vSm2= mx2`ȇU5򈵡y+ opӌ2E1(3)64O0X} DJ[^`io~"X2ߗqT$Jۦ"#qP o"O0 7|N$ck0Uoc/gp $?> B G霾?`Y(hr 0F#NB{<$`&ΪdO[r ~ep[F+gp+?ƞl}$#ȧ:ŗ o"O%k"oN8d/(7=jdGq0en-?OV}M' Azh1œrYҹ$c,u5M ${$h :`%jCȇbA¤yŰqbSMO6crM_T)c;u 2 7ɘk2=3K\KTJY$wfG47B ,1MN|&OG10X6Ky:y`uyO.Uy.sçL M?ePC4qp=c)lB:7hJ A.j9Dޅ eޓwR2MXã›81*dY u ii=)d\rb`pqkW>|>#ף56J頭0(ۣ 0F𵘣hC^~h0"2YU|qqQ;͕ɧ%雸[Gi~, S$\ުWHX $ y$cSJ7^Z~87yk >5h([s>.͠tqzk1^ .ܞd7ZL&A\+C8XOU.ubO8`{/>l[xdU`-ZB,'5~sлL_8`w)~KfgC'$6 L)A, %Yͼ1JtrG lÄh1RDLQ EkIi9Z1ƗƲ' Ru/|}yD}j, ֶ(5⋖ԉp5(%\=h_7jd,d`cQ 'ro/[r7 3nkzr } L#յ@>O57ؓ>O(V8ĬSH[`]T(ӗbl7xwcP,6x5u! ^~6U7!ɐٹ]U,<?kt n TrF[DepBKA7>~H_Tqm*SZ:c*elظJvwȊ *'9Ru)|2 4ܶ$8Ae%'gq ÆXshǁ^e{ƈcG a^Ӕif 2cD \ZR +Bx#ShLZBGm`n:Q5z߼W6xטܝ;}(gURxX/jcȺk-M?0KÄ]j"R9Đ88 TC y"ƕ(ppGl_>C{cd][\ ##Ul 5ߘtjJ;rʼn$"hʃOXMvl pƻ5ܗX,;m^ ̉Gqa(w6'?}Mqr 3rAy6UIdM'KrH!MftWG1> G/'.765gkvőV9E??Z39bι. ]3N!ChQ:i`+A108_= #0<"4QȻrrNP8S˳Qrvh6|c"@e^X{j{t0ZeHo,fsgSi@e}njmF霮|?e{ƈ}G@_,ƒIcep3VH+ /&>\'&M6զgO0pJB+͢fMڞy;a0뎤Km~7anՌ}ÄMtx"Xȗ($ymkm{ Huη!Ԥ l^/!r`E$'bLbWb2910W/xbr-i9J'KNdp~ 89ٗIsUhw.B% nv+8LOܥ-|?Ԫ+;[Ә7L9KrEYI)`QS9L3e{&E܆ic7|,_XW7jT;yB*-$vXCʉs3W~:7\U/iJ I=?S`ݴi+5QaBn?QٚԎaYȎI-=IģL3mh4 lRo~{7)raM|`Z&[{wm&Nbp"# <|ơTArӉ匇ݷNܦKy~S6UԤ!0X`Gr 6]Nzsڪծ Lμ?Lf<-F޼ҼS!%Q:g }~ܼ#fW;k9;ɓøz⮏ ]9I+'ɾՌ9IjQ FJKZyĀ:^/ζ9rn}br}}Clפ=Js\A[[WdQpءƈcy?Z*{Zu^ol&r1fq:ㆰl&:ĽxgTF T«811}m/h@!>JMGq> ʦ 7ajQUij:[> iFXכw;00֏E9̟&sVt5qp,.dW z= Ea3N .-* &m2qaeOs g9lI%'QORE[rb,6a0@7E}1c"8ec:[=MS (ǟ ft=Kc(AIm{׀M,c#r3(8C$$WlGQ!ēG󲨚}RX$i`ߦO`pNj8㯸=8Ams%/O8d%o0([o+ZGq508ǺZ(yZ~ݺOet*>G4#Uj0:4@):3Z1ÍbF霎'x4h+ :L0#u 1Y:)-m/ a!{ u-m C\"'8CĂ;VBN*9pL0%/ȯ{ab)4(ɤ/]Xr;*X`j ~-`V:^f:a_>+ "%MJjQE w}i,`;J頭 ]5q0C !{D)mA9Tx|yG h uBibQSbǔfM b;WɤTQ&; heXi;Ap,17l(6oMS廳ޤp8!P32c1R@ (s)rmA>S Yn\:kG/ 6ZᾸt(Þ#q`9%\Eɲ/JߒxupO0!, yv褞uO$%jR=JDBd`J&wT4hb4w@`510XH[\4M&xLJ'™V&fNSq؆r '7h+߻Nd!ZhFF(ws9,\Sw$j}6}r,NvȜdbivA"sQ H邙"'2S zmmbvfoh/V&Fq0& >3nJeSSP޷t>h)J/]F7i.u>`;hK1A[aPG-@`̰s+!6hӪL]58TyW订pjݙ7{Pu_١zNF^Q&xjYk^iv8{:Zgf9]sr Z3 ׂ2y]V%#*JƸS20F `z؆(ӗbl]wv Cj1CY{>S& ^c~.Ȟ{:qiS(  ݴ:AK& 0F@,_W<"$ON 0&S"HcKQ;$FAqpi'}3W^F) l P bd8 ct9I-vNI5A${s}6gsӆ/^/>Eק: _6ȹ`2Q& f]E&cy50,1EӗL_"`''g9Ɂ=/|UT fv>e:^ď:2Q:gd ӴNحbL.)ts CQ|JFE$aH?g6C|\HdZʨ!$}t34B510UЂ%5Nv4b{2ۄ:Bh o(YOPe4$9ӌSNz $23ll`؄Pð%HXk(ђE.9i/Q:g҃ lD7H\4Q:` ;̄DozHR{rܬ+4CXag\J.!E>/x(aC _MS_(b>(`ݦin(_iLyd'VO+\("%g=Kq%NTdfPsS2F霾?`1z5 0F;{N_M͒Hos5cs2,~H^Ӏ|"}\IWb[~-ҙ!a,Bn\0X@W7˜$4!L\ NP*H(T⢛B9*EerDrmL1O}Yy-tΜmA>_ nV.PPD&X𒒜c*E fջd ɅWdw4J?ܞXYvۿeN[[] c5dhuVli9{`stV}}~ C`2Ы=}<*vqrH! svR+=>`@j<>4Abϯ/Q}.gز,£,@"H `݅e%; Թ35ݟ]T{=555kee+FE x`u vo;܌a)MrPq\\S\Mh2{uLoVɹ}ct9ZZBXy8韞E2.)[*7+6t@XBp 75>*cdQHLxq,.q8 ^5=9j0ˎpKk<9;V#J\P2J,^ּrpS׈~V)*5፹k8[`^潕(nԓےtC=/(byWUX^9,藼Sw7>:D2eU2h9ܛkTb&OS.q pE0X9H5FGG:s/{! t\# ; F(H4gUh0:uP3ĉZ4ni@*Aw+'bYY,Φ #qٜ7N/_0w5{F"c ƻjO}+s\! ]*/,jwL}Ӛ/ت!8Ck@4wCDs0h䜤-Ur G:PXV(U0wd_H2,|pT+HtBL;#L+^Ϫ)T0MLe:)3ٱ0SXw=fV8Bm [, i0oߔ˃kQ=b7nd>-W<'cꠓ49DZ)ה4+¢ IsFG; +TCY׈c`7 t:.Nq0td6P*c)8۫rtq}WHW31Mk`sh/U 2ֶ`ݐ(#^A1'&0Z99<تQ0}N~ftaqCl]UyҊ",8@fUޏ;y]\)ҋClGdQHSr[^v&.cU6Yr2sDa H 6M"/7pxʓ2i3xe%k-sztՆx!7>}ziˎr! 4o/.4xYmnBxk|LL /)s \`)6X(-ə6[j7x3Dhp$T[>l>++|H0M7f`:`J.˓D^'G(eޅaaI~*%ܸw"Ϫʇ;hReƈ7WTcVBLH0ȳ86XtǗ[\e7&Hǻ+Op61J#@x-{%w])>}FiMVNNַj[9[RNm1 "jdVE+P Ґ@ƘcM8CiS}L48Pݿ}ΤVR|ģ@'gܽ]xG娏op1Qi̽0g-3|ȂQ@YMoyGaFKrpd 2),0{"3|sM Gu03^!o1,Vb&X*゛+QZCOq45`%lY<7` r'S/\_o:c$'5pOa1Ψڃ^t4ۡ)WkD&t O9BqwB^*wʼb<e+s\ 13 wǝe%!z2q6?9WMǓ[Wh|g2).yY]`Tm+`+#52At9) ~YE* gU)SwNg5G/I7kjzh\֨M!.F-".)AT8dQH3?]fipCZ0~Igl$yD>wza@rvսb"$8 >~vWL 㥝KgG7hšQh~,D SLFwX#Vs9CwߏYlhwnBs% 䜍ŝ4> v2Ǖ`:  VTq&>ս VN1,G1: Fնr8 2R\#w\|I,&8X2֔x #foR&`fsu"nU<,P:&:{!)]`w¨q0:x*/{`+aEa\jUki9`-1:~&E q c&M#iyHz3*V6: LWꢚu|.Xx},>55J %4X0D) 7oħjǭ*SpfT<޹\7VܻZ iܴ+ep`6_=9LⓋt/bz0dV2+Io1ȷns}X/.n4,3+E! .%Yᷕ`M yJrBr Bq; Nl%dq+))Νfc,g`p99H5FG<4i"0HlrN y&x̼Vꩀe.ͅF}1,<:}),W]1L q4&SK_})o51c%Xy0[L̖6NFvk?STllfjw~2DG(Fn^Л ʵ*"VI/Ӎ%=>2opb+۔ׄK$2YT`;p_~u,[ |";-g#9w\fn8(6!m4xMj52ʪ[_8R ӘUk U$P-}l?f-0!Onr +s\9 +K'õ-6''WVN]tjo6r`s(w)k,֔Ϫ{U!~pGA5ae(#)i{Cۜܶble{vozݬe*eBv[NjK! y3J.!ҘU:@GxL -a(=6fjB,{X3.^Sz٨[慸A?}T%`-]p_`TOIObK^Lv'xܺd"#[ئ^B鯰t>nV&Bc9NsމE^;}qghu3FlAL væQ;<&Ur< JǦij$tIeY{tmu`@'[L: 3۪{q3BYAGPy>Ī`ä+YNB[r48RyDu}#8V8%Ί/ߦAGۗ?;°{%o/g\=dt0)TqU/ ,.jx %SH=d PW&V& KPD.0YlsMna)W[rDvaʸשXY!FG 4x=l7kkDᱍi0[zͷxrZ ~D`X.LXYfiQgLq]6)uC|DC\:3 !Isp-ոsَQ0vX+cLp4r]e |}#`dBaG N͝Nv2Y`!t^'rY` i5+ ㋽ų-VNNַ>mgFQ6n*%*Kr6YRH?SP2`!PJAUy# &Q9f㗦=u7, Z99{v݌QkoGi*1uR <\޸#2Yхkjp95\CW>xὼvg/s.wؔQ@:}Ur/kDᱍi0ԗK0maFj2`LA$)/Zx;#XJ1a²,ˁɹ}ct90is5\#\Wٽ|Veb‹")(86w"YWd1KQJt7~&*{W3-e/+'粽5 ^\r5M4G|9 `e+Bk [K R~=gֳ3$dӖrr=+ 6: F.Ŝ> p4r]eY)3J\X|Y -;-+E9 ,cpZslov-kn7e!Nxɹ`;l({;f-s(KrrvĴQ@mw9 ^ZQxlr<|!f2`Xn  WH)<~cekjCRrW+'dtm rle=F{JZȄ2)^,Eg,ɢCnὲH9\` a!|7(X3VNůʮ4: }YW㲯2Ns({6iƀq8F^8w@o謷2Y`Bt;zlo0wǙ rr.cvozgݬe6n*|n);ٷ ?#_p`.(bẼ 1Mk /!RSBH䕡++'6j[9a[)EOF^>۩2LimL4paƒߔN!nIQ>s'zKietP۽Kv11W5M4n2` JLb2i/XmԵrrJa`TQi/uhF~YuQvvGdQH%2RL`ކc!Q %*rr꽰({;f-s(G黇Qp#בRiQgLh0/}4xS`O=fTzM{:.%m>ip uVُrl::.ۻiQg̏K#OG?~c43dHWX1 \Q.wQ0v'J>}O╎@~Uuk K#V?wٙLcdtP>}>Y֦Ǟ1qo~fr kt 0Oߓx#P_GUp]4B_PpZԶr0ʵ:];c:c59֦ǟ1gOncp~~\D`TElF|d?C`צ cSOnq|B~o=*9ə*jv>}8ut\'kcϘG|EV`-0: Fi5]|m@~RiQkq~Teg2=Q@c{n/~c 1q^MQ`ms(vi >}w;4!:24X6 ޽*}W?5j([ctz_G2Ћtsm|`o0C&~XyYl=@vi(U{!>>}ٻr<~/Uu;^kc#  է`ϙL#etPN7ßÔ|90N7Lhm|\,E/`\I`TqWif o/z@\F`mlY Ɗ>~%""HF`p&3;>CiβeM!7XG݌1 FGVfC~ Gg֦=]~NHM67h (mgGmmfk#w#^p.igL:(9"6q.%`&-W(U|\tn6 ,I0+GZk#;}"6 ׋i/t[d;2: m*wzqS ߹guQ>Ǿ]ct\yq#԰_j^{9@D`m|߼Gj_ÿ'ψ\pj0%:DnV"2g $Oާ7X1ȟњ8X[Gןk1(.p^ou&Q@pV8 `$B0 pX{$,!4DޮF}Qkݝo'Nq#!kJ?_eiXr&-6qW>]I: f8`&(B~v?#!k1/PbR3JL`[Q0v+4x9'zڏ8]!6 *_UTdBڗso24)4ːw`Y(tpj0%:8 .A)'48Qud4x˾l|,>xK+rO(ʧ6Vù8w1: }{GANS4.lԊ^43^aлaQ{qCn:[kme_?_^,LAMFG}o;-k4=>kcϘ!(-VM*n:z&`TFD%(d9jZoٗ%邉_M"X^3 (mW5v<FVצǞ1_~K8_<&lbt0LFG=xNKP8 !sz7GG-6 ޲/HA>}oc;p[v/Ǿ]vk[s\X*48EF% >Mcvx2aPQ{qCn:[kme_X(jU1~oމ4[-Yרu^㦣A1: FaKtp\RNipֻ9:nٯi}c"ণUŌjz'lW/l[wŨ]oiϘ-P;Q0hipHH(" ;䟟;bϞtaO?IObϞ2%Ql\(ׄ 􋟾?.I_8 9"6 7@M (Nφ{C>LpH͇,WXջ̷r׿ Tƿ~Ԥً(3.TiBS0?j>n0jJبMM}} c}Q0{# +j@*Y|"I#<˷JA i,BD`ZxH3OkJԇ~c$^+#ce7: E|`iGo?pPPh_xE{ $4o%~*emJlE& 84nM U{i 2Q_$ƫ9Q+:WE`m3;yt7l\it}8gCǎ;`<8E'V E`rd+埽@c>ct1پ(U{=NMڗǁ@B)WqBJ9ͤfa$I 9i(qaD粱Z: u ΁^{U79F`m|bzX򖍎jm(J: E,ߗs]k?2ˁ&']HeڣA*d@\%1- (ЩJ򇪖9JU7iܨMpW_YqoOU-oy7 WGQ̀V@6!? Lj+`QuѦ-b1[se'4BOpŠA6k46jEs{#8QnTm+K/! 32UbZo\ gII6~{5$'ƜSI#e|#8=#`Ԁg;ipN4B>!A$iפU)|22A 8GQXl\v58hf Ai(LLd( "Lv8 NCW56jE'JGp. ܨVN--#; 4OWWaT:ūsPGfF/~5,H!ބJ:Y%T,j/Bs9+DةojZ\I0GqsBeGp !`Ԁv mEU-# F6 ,Dw'qś 5%ۃ,:V/$l"#ƍVad?XKtDqѓ x8iB)FNXwd$ΩA#UT ӸQ+Gpj[94!$ŵ;!W`\,HUs@Z[j`2O* 4K SP8WiR%H&8@5+-@@?³ ^50-iPq5fEZ̯1e!ڌ2v46jEu|#8QnTm+g㧅R Aң V-_%e _־XlG']tl`xq]^qGF 8j+ {Vip@  ;)o+=zW!0邟R`Uf@ D&tARo9i Q[Fmt;X+lԊ΂#ܨVN'-# &\P"BtAl \]dBZ I;?FFB0#!ي ΈH F j_}~WBynr{#8#`Ԁ{;`; d|"iO^S=6~H0<Xhl0 9~Qr&ۿf$v~\LwtĨpKF73ipHN!Ve* .\p" k|^,>}r+DŽizv_vǹ Or>X$5i4NT۴,{R&^G*aQD #BGmf+[Pip`NOB!zJH\9̽gƽ6=g8 +k}MЁs>X$&&0&0 Lˊ fh,R%lԊ 8%#`ԀUʙ4ev'yy-LL+|2-eNp}PKLkZ 34.G,as82` 9+GG' F4X_ }_B lAIFoy2ި@qL7oy!MzohSvQ+: :#pI5Fնrf9 ni0'q\)FNv6T& &Cpߥ86o!U"x96${DZpt JŠ"iXm"E*a7Gӈ{#8#`Ԁ{;`; !'KJ/Bk:EyN# -/8GÓ3(O;*7z)M!C@>MۧP%Q+vˎ#\&F QcN[Fjw,dޫTeo'q5aS.Lk<sH[(ϗb;8rqA1i o9"Nc0 76QG0Q ^4s܂4ID̠P)=gT҈ Ml!\k\-*^(B :|ZŤJبUM#\F QTN[Fjw )nү}=Vkb`xGEOlp^ߌe\4Eq)d4XC;O0J͑>ܾpn0jQ{A-+4ezO&a8UU=G@ k\-@,5"Ee{*aVte_uG0jms2RVh|9gWRIsĒ]H,Q$d6#(+K$acmIy V%_%HG޷:#-F 8jn6GvJ!~8"x+(b"fظD|}{j![fc1O"CFq焥edpm9"Q[Z5+UUFouG0jmr2R=`8}o VW sV4ˎ#\&F QcN[FLj!H,,V1لdM]> C_%0X"Ih&BQ`8x>VW sP7G p0jQ;7?ݜT4 Na'PƫsR A:D>|DE߽Mhw~ &ADr8o~+l6jEu|#8QnTm+gw牯78K/˳d$xVyQI!#_~Fzz7[D\x#80jQ[O @߱N[ L4 !]Hk*ʓ%xRmy¬AW z#8QnTm+2G%W$>$ {/ OI;UoWSEG)cTQ #BGmݙckzipˈآ@M@LUR%L"n!I)68=0N 54mEmJxByF*aܨa#\F QQN[F o*)'qN" Etr /_R4x]MN^Ur7G)^vGF 8jljVTu2Ri0 Ǥ˒ n @D} HymgVZ4,42j95KԎUFhGXpj[9͜-LOK\ԗl\1ftƚdPDT3 j<|Vv#y8@5N*-ؚC}%/V? tpb]>nr( `FL>~98(T]%lԊUGp.ܨVN0-#/ OJbRbb)ޏlRyQJR3 !rqAFeuPws4@WGQ峘GO-c/ (ЇU}*c)Oॴɋrf8v]4>ؔkܨWG@pj[94l D8@ss|yh8l)&s2 C(Ud8 VlZl'Hz9 tGpVfa\C-/ & J/Oijg^߼+!K.eBl Vs᪖sWmܨa#\F QWN[Fj_,Nݨ?/?[omN :~0{MI%TC*t`hqʭj9aڸQ+Gpj[94Xh-TfD/cʸjC)W%ɇXBIi<:>,j-OT5jW5h.tGpf^: n}ik?[~ vzx̱6%u_D)s VU-O4zz#8QnTm+ڗCX#q,968sƽ B|4oa. -Q/8Pʤڹ85=@8 n}ih.Q!H=ZK?W_ I۞܆eT1>_Svlʨ{pE7ipH@!Do)PpLP5^mM g/*~ ZFocѐB *GϪW;6(BGpl!`Ԁ{;Ӝ`; 4{JOҔz , F* v,&ÐaޡrK;dH8BSI9TՎQ/8#p5Fնr9 ni8rBABh kûtrO!0XN@`,;U鎋ws#80jQ{7o!A?TuJwW.@ F^q'+dkd*ar_mԊN#\F QZN[F /)9aU"&shy2XQ;_1M.Zޑ53?@V$|9P;J8Hxx/`]sĥDh4ҼpG`S^N[FLT- \.,>2U0F2I!2Pkbg+LnnBtƵN;wlE )AR/&55y tGQCUJ: n -9y_(eGǔb˵;r 2ӟZ5-69t cj6W G),+B|tCn` ~)v+8i1 ^t܂4X(b(OǯMޤCsX][{4_%5D$^]Nw,,G&A퓏_z1ns>DzM^p/g4K-4i0[ӏ~?6#4a=kD'/>U\hEhh,߃*z|( `tƣpL7Q(S*CgvU(L 8OmTap-"}kԎwڝ|8cQ irK5rN wX(ժsGst_y;ZQ%B =۔ 8Vp+:Q &ez/$PS tN b-=Fa :mfk&W>Cm \җIsDR5q9nm ?iY;MDuBIF1.<~x-/Ђ@F<P[ҵӅca2>nEy)!a45#1>m xχ xU[<R L  'J 1]5Zy/RPw'N ï'ñPn,DPr6SDND$08'#>mu2'QHT a1e%x] 72^LFb,_͵6R"r)${PabC~G>CP}}CK`ڧiΦig8"sܾwut4+ݗCPĘ$MrS. gLoiVAlvɗKX2df+_>odÈѶǸ:8j)vGXD8vi0Jxn"Z΁i09^ L|O}[ UrzU1fU~ $ j rҜK8ߨWAٞ5F՞ND Ď4XDz@|ŕtJܪ9Ih*-K63sH{)„)ռ&gMkg z 7G9]2jQ{CMD˰HpzCU|t )X h%y}q_#'=Cm4;k|czTjԊ]#pQwQíw֨7>lu2;D6z"($Bᷴe hRuԆ Kd D /;#s\cF 8j/5Enhh0|ZG!9(Vyv)Wj$<bt`Ss*Z}_G4>Ǚe7jحךl{TcFV| l@^omZ+5r[UHQ@ƒcSXRJQ |31L(iԀ{;CD`# qU 8jxw/P<, AXUȓ3fP |k}$ S_o޾ ܕ7͈W0jW sDV4;Gqypj˾n"ZߑoC>yKC!H$O*|RXD#*NևV\=`K ʹ]QC!#9rJ0}ts49^tq5ਭLyx#hi0tҐwW Nj_"Oy<|Lb]>wz>ހG((E ]2C'۔*iߨUM9[;ܨUCwj3pMR)#$Ӕ&1 a%ar&(l$_*"z S_v#vtq^*5=;K&e{\)[" nXX%+ ((z ʅ.9u[M2W]%lԊpVGwQnTKMD =`p_V!bSdMU;.[ qۯVOo*9RM@쥒Qڳ^n"ZLqPWh{G`~v*7W\;o__mrVe!9(،Z=g|Tߨ7#f[n"Z,@ &'@@J`z.i\oejK C&&2J/lqcpvoz'lwH!JiToXBB$@毼L TvL_e4?ϩnԊ9C\Aܨ㲯wB{V#h?} {L:m|R5͍98 ^jԼG|a:(Dmw hvw$1!6(+=E]q帍S;xoi(ZF}wG.>57jԠ`{#(f,!#Yn"̦Z]pDH7ǪH79"s\JF 8j7xsMD =:K gOly_Ze[s !sZ[UܨkRhALZ] Gδ_|Ö,#[ȴ*}t-S͑ouzCFdG}p.Y툭Chh0 5fKQ ɮAB,OJR⇯op pNrz;\vaVt9X7ܨهsq6t Ԋ2-ȠLx583oV78x?7R騺2ÿ 5wOQ`U?@U9ґ@oۈ쨏Q ^qтNU- =c!yWi0am Tƿ[{l"9؛P`}r?}M~|9EHK>ٶծ3V4 opqG5Fծ]ED`Bt4b8X&"D|i.onX>*񭯄Y8 qR~J# lB\.Dhf9n%`-6kԀv[쮳!i0ȇ]4X>~O.S[:X# 7츕uj8csjv"rx#px|;wШ7v+&x,|H)_?LH„i0 R)t}aY nX5)qA wǏ#p\:0jQ{EFup2p: ,>̋f7[Q`\(]H|tE A3-Q+Z9C=2j}0m i\ܿ_y)r` ckipf 3:fwYC n8O~uV#yzT8Xw2.6F+;keY͏z/iwP7w], d\_&^"qbU~YZfU2E9=Pj$Q+:@WZfm؈֔ۺZEXd/UdDzq2 0xEZaHNS*)L;G{I`@a 7$7bXL_nVo5h@ں9bq "4T^ aK/Z*l*cB/24_/x\,WR}sY8JMYhpL_L5}Z1V^T!ۺYۺ* YobkTqٖO x}G%UY֛N˧H/iRGl.4E',x\evsTK ),wvS;J_(4n2-j{;N[dj M΀@2At85G-p/|7jg#;:@S C[YU{q\e-xN UgX˳{HfNA`}9.WRsS<@`,q%9.gДNl]?CFf0-j7xip S^`fyL 2Qǔ)6WF< Ϟv˨-읋99 mBws.caέ pjo8Mr_ n &LTc%eD&y>&zJ0xEk(g@\sMUn;|/ںUlQ w#SbFEm1܅8 .jRpjdq]=D-ʧf S3͓W&kcʵjʓp ~%72H*XJ|#P@4֕٧NŨ7v'>Y!0^_>a̔Q>5 3^´mıLdZ1Z5dcDftsE!Pn;֙OM< j|x2FEv ׂ1 h7NSx2I+Zz+ {6&a/<ئ\FF@9]\\ ~(D@mH5 d&.%dS"Pht 4{Xx긑P,^0-j9.> 54e@JB SMZHf Z)f\UiyԗsYŪQ"L/jY2-KCj۸*cͫ)b8%nlJjJlEo#U)N[Fpj@C rW#J!MsHh+Ė eJ. !~[Va(XPK_\6E7ۋҸ±7r:Ŧ6غY\772C4-j78-L 8aɌyc[?8P,zF> kDҒF̟yȾEA(M8԰hԊ*X&G[4eS+02ndJU{q\e-xN U+=}rԀ3JHSDxGY !E/!$wxI-ơŠĞnRoNiMNg i0hi©'R ԗL Hy9.>r WuAq! R?6w27$v8}yUjA0F!DШGٷ::Ŧ6غY$Ff4Qe[v܂g@<0TYHJ 4Gwc{!>NCmg័~.]y"Y8.__4KL5e]͑o=*N1k72@\5-jozZpM9 n©ęX S-,~۷JmCA%'3jE'╎@9NiMN6}7Qe[v܂g 7a"Sq$#t}N^]ؾ@̑r-V)#PhrM/\*R>ch*a72g G.FE}\]Q.c!T.B*,$YKF#O!2CQ./c;F# PhTNTq)p/gUѼJXigrX^(nO*aq#DzuQl:Z[4©A>Lxp$FB . {XJ[&Ìdi'3ZUJ'ܭ(z9RMF@u)Th6U '݊ӷu UJ;Mb4H6.$[{V KndƘ/Uc[voRcLkSb*D?7sBrK$ %{-. !X,2C7rNzVTorJmbf7:qG-yCjU t /^g![IU%Lndh/j[j/8p64٩Ag©A'Z GWJ7)#0ktl-uX6&kxIAU*3FM᫚|\&1T0-qJآ시8 R؊&Vk!`iГ,ʻXDW5BnmZS/8!:Ŕn*q;>GK&<_zR&VV U{[A-Sa|(e(h8hK04'H7)lvSs NyE8G"ptPгW (7-jr\r1&5oE'0 Q*D *@rg%yq #T,y|ov}17^}aa7jEszG)lvSvB}I++WT AbCa*aF&x(F^|Vjip SIA fh+9=JF |['_6I$Rovsިy=#Pk bUN27L.\rH1µKRP chFvDbLUt*W5.֋ ve\xBnR,<X6»޼g(_Frgtںr% i:I*LKJvDqKAd;U҂I$4Q qr i0%NBԀ|fiRn`_f4o+Q_9t8\(ucS6[Sh븨6CveknV K1BL^%,Gt#C(͉}oaVL o}:*AXQlذܐ ,,BLZ0" fY &t6.F7G On79D|~%T9@ ]usA)6-?%fU W$V{m,^]f1^~472:>-[bڳ7}-\N[N©!‰΅%7/!6+r7K9lthγ?-p/Yx* Q*o\|Htc=3zz\4On(3.x#Pn.0TlZnS9 e* 9JyrMZNibU5 _?VdFFǧeQl2R[4m}jH>aߐ\25p{w|C!%mygQEiD2$45e&sJ;P v35sWo~S*e7G@|Bm]k>=\<6%%U&N[G$U´ƣ|e92YKSr/ =st#X`[MyCt : nt}jH-! l3e-JJpj@yh0jH/ȅ_zZgOC%Ҡ3k!y StրWl)) q*zJ|#Pn.)4_ IZB[ǥJ}hKu-[04{Y" ^#(#ޜ#,<cBdFFiQl4T4l}jҋ,Tb$wDpj@y#^ͽ=]_qqH$Rm:aw@z ')5W+ڞҔĿ͒ږDhLg҉n":i/8Ҩ{[YJl/,7b܂;-Ѳ .쇏hmᖺ@+cŞUR* qٍjbkTFmv \85D& I$ZbXfUw6I<^vnm>_f1DAUEe7G 8:^_%'Rr#%45;b񲝄Jx2=4pY֌ndƘ,Uc[Ԗ~))©Abv+h`ȇL|j5%BmSMh6Eh}>K9L->ߜ}diԊp趎+-`ٮoәJDvo~k8r*GU£+0w-?'(LjWe72 F5÷RN[էlI+t H+p٩!M;`x@"W5$BXVkq>< J9yj8^(ֱ5\GSCn8;5BW m0;\v]Bk(<3%ݗNT >|AOB<6l4eDGD-2@1 mܱ2Wr3K㩷OSYjS k{QlQ$N'a)ԧ/)Z0ѲPDb L@x{ YV"Ot j# fmvغ{4xu B['8.j[y57΍>/]8d|=(amjw72am7Qk\25(_T85i;P,^0Qtl>Bqz)*Kƅ٩AԎ{pG`Zs(Zz'iE*eሩ>3Onp38'}tsAS9^&.:q/CJCؒgf0Pk }E[(7t}189 o; 1jQ[90u2S,X`50;5DRĽ ~, XmìՒ=Vt/^#PhPhƅY[REcSI]0cM x3CyAxwe ndR4-֨ˎz9 npj?K} bvjHՎCIt2-C)q°P`uv$7,(3{rL/I@$X\nrx(uCX%2Kj*[Af%~x|zN|g,#>-NƅVe:_=IkȤh,[6-j8.;4eP xJȓw5 C 5`a}!UcU̶v֨MeG Jlw5$Ԛr[GDAԜ˖;V\Xni0hy6op׍Q`V46jE&G[8sͦˤ±i>_[.A` -!Xj&nd&a\(F^d6hip %SbM%4XX!~0U)Wbv/d<֪q&DMg8dR&57G1K@q#$Ymu0*Q/-+W +LnxJE"H?)&Qld[vop+; VT85(S@nS D )c!rLkWEp/ -@[^gQIȰtFx#F@!òpc.aN\}yxqz゠hŒghFfbkTi^85(S@nSԀHSU=$E+ ; -,"vU@z! k,ʟ#t`maEˇ2q1Wh LQL9ҫ5ɟC D }34q#sƈb[V3}wNsȔԗO 9/Pu\.^۴4©SߊiR>5cgmr*?_&B#YۭX#YnB['Ϗ H^0xQYe劖U«[(̾[v2h+&ܼP;5tANPD"X.Vt` 7PXPmVtqCES&`IIM=Z\k0\ĠH]0q/㬑yqm,4AjybkToO |&#>,Ĺc\tAߗ)LRI66n+mF* S&UPCiM$,^yxt[-*k$FB .u}>%_0zbOYN`@L-z_ݶD(/grt2~əkůbwgq|jhv_?&Ls8j;1|)Oi(<TH&V&nFA}dԊ*p&Gqq64 ,X5嶎ǘP 4^I Tz;kndTx6֨MCN[֧BSPʨԀ2;`LSCg`]K6G S藦RA#L4`__1};,}zz!.嗋hIPhKMe+8îLs>"¤)PMˊndڑϵ`[ϙ\%(dA >L ?*0}a{1pj)T=z„q)Wu /?uJwWi0]C|+FI \(A@u\r?m\(u{+[yC:I嫄ć޿s#*tD@72 bkTlw-PSCjaeNS|\T`P.Z/7<<, [`Lpϩ3G&"2 ~Hc!'(Y[D`օ"x 6GsW x}i0}mٍL9FEٹ 8 PQ0ĚUITm0S:W"y~)j5QFwrPdxIRKu۹Xt[Vb_{غ'APn0iҕ8`VHijDTrWF *aUjhvvpO}O__o3o{Nh0q8a[`7 QWêK<_OM$,^yxt[a`ы}KWlQƜ?.aI“bOnif)H%koeOQۍL={\:rN1r}j$L#vs\55|%zNoQ`u#ǡq Y[<3o,^b?qXFh/;g :^@ruDÑՁ}KWmѣ.\Z *6:\dUQۍ"O6b[jOAN[E*>7 ƩSq_X"S` 3ņ"&Z0F!\ȇ3hॡq;.9c5nU(rKgCH`X&ebe02W/$Գ 0(`AQۍ̂82-j7x0 : nS`ăX&.BʓT6N -k!5⾈QlS|L5L@MDZ>in)[56xRmɍ6M.QvǓ1qhU,ƅvа9sXrpu&I6uSK9 #AVK{M]Čʨ j3s<F #ݠ^t؄L'0:{,$:?80\Dž]t2vZK6ڭֹ|="xgdѡа@7wUEYa48jA ȩ$ؕYdMw4 Kf=F_%!ʐvu-0~Y9h.y؄t2Ǚ5({ 2:}z {Wco>%iYS7;:8"3'udVe0WXqjG E ɜD 񢜄C!l })x56X$C,\Ó 2MM80l௸y'd:,۠jgǾih`*@F~ {Զ R"ʸϴޫ$H~WdDlvirRE Ow4 xf=F_WB&`d9tU_'x9-1QRi X,xNI's[Ծd=$' c#„W=jeu2f|5ub[KOch2$IK<}<$XzQEy( `uP\Rp\Gl̂K&$1uz J_6y8e2ûr(<۠j4i' eh3;ș}D(аGmCw͠VchdTe-#=nVe}H ϝVhI<8gkXrpk`< /#7ųes+f?55L'hPlQ[`& ؃LsD`fWQ>UZ F_',t4w'F`V6|z,fi԰ qV`wxtWz#5NfIUՎ4xơ<:4ȣC{r.!T$.B<ݑNcN>5#0y4N`#,5~#Rc#c ؼ2BxL'3 K̠آsA]+$i!ҙHPsE㹼 .U[xŢu]bElP/(%Ns=8qtū,Q +s˷% "~#E o=FtBXoXLAm&:s_0]F$YBr~Pl^HHg@8Dy}00]e*ihأseddLk!#q,fC͸YT\N?DA5Y8aփdĿ_ db޾7%3< ǶUBP9jk{v%C:VSVL}AN׽| ȲPwL)JP"`A׉3|7\8~vgWdM&$4D8,G#i ,2^0,&ezdt2t mP!I t]$FnW&LCC802Փz@tRl}StG 8y}V:&i0!<‘l-`NE %΁’p.|e-1o:q 0A:t2SLzT/(RNE ipbCBf .7eX6h"f`bdbԋ*-C)lЪU2[[ -ܱF1ޖ/pW-xCxBz8|Ed1XPl@4xЀ9yE0jN V=j PH.1S\U_'00Y˧:n+7=63T9yB4+|C(c?1֒AE n|`;VӒOxDŽxuDV2 ڗE!g%)-\PִrgAKɇ1~|UϽU厔4xơAn0t2Xl4uȐADlg}:u;駃 Ƣutl:ޡh XF)W53:Mv72s&Q;gtY!Iga1fZ&ZC]ފ1adj uٖ#z.pи~qn5ܝx dp !Un$ʻunbFw=UaE^ y*H$ Sv:jb$ qh(+(V0 CÞ9WƑ_M_!`BIVaf{B$. Vb^ pyrS,kG@\~Fpu6e]Diz1YӒrj9r J̝uਝ;M4Xg>40x`nIW@D 35㉖b QJHTA,(nʆ~1L\lK e:S`$(<ȰH1/ oͧjÐúgEKfPld'Id}h!>Kkt\ >4?й^&z)xĽH()'9U;,ouF\c 像DCY]Rk/3JCTca]< 4T8(}k ' c_}hTl$p,C;~*JAh?RE@u}Q0I9ɩ|ׅy@욧N+w[ۡVjE2^TiQJ,}E=6oYwcF &J3=D- JyaD!,ɘm.۠j784Vckx0d44i%2y" ~ (m~o(8660dJ.Y}Ӗ+ Rfe5[4o/Er׾Į.9ϞAE cwܤ:>QP036Xr(?BLϞ{+,D* EY()]F7$&n*`hqW1;_.D*&nD2 mEFm-۠jZI op>(8:Di#\׸BVsjQ.PbZ\M:R{6{,uIm\DžaBs/x)qe~EmatGOIXu?:KLXyvipi[Q‘x34T?ۋr8[V LaeT+a%3K0m3;|ʉW\\;Y d: ivve)\x ׭qr"qq$ ̠{pDIXMǂt# h\fixY oT[ִRP/ڊIOF辮?pmr+a 7< SmRPDOAb vB(;<}./^K,Sr=*wWt2t mP!I XԄ`&v%+!-%Y2ϝF>W+$:iDCTMTc}o璘: $@}yX~g:gws6x-g% TfuhC|0S24I00jehX.:!p{IMԋe"؆ױ!~"Q!UIPn=TX_c*D/^JN'3 K̠UN4xȖW?\8gs: Na΅ʍG-_/ݑ O[N\W|N[.4X`em%Zc}[U2Lr>Nf.AEm}~ 8OVH= ^Y:V?%d(I\efIvxpɼ:#e[" 6$3|+zQEy( `uWʓ/a׉@[^oi s7.mxUr:!z}A v_'-ilC[a!Ε ؑVw460[ughyn\. QܞpA*β7S˽s vMJූ2Yr:Yi-CƓPa$x gxtg!`""=p#`uBM֋OW7uXA"o,voNrkɠآv^>iiI0Ce!#"dE#ƗmExP/(%Nh0dU˭Ϟ(ٴQerxa6xX[WdVa\ (AlOLp`bI(Y%[Nw4 cf=F_'pNߕ{zF&_'ʭ`&ȭg+ʹHN'3 ]̠آ>0u Iơa-MCÞ&x>8@^|).|dcFn{s^.6|Wef(#}a㥳ᒆ+`hw¨hwZ%B,GYEɬ!xPlN瞙4xơ{GeC[ge3UJ %'Pα)x;FleeHewFv_2U V-ʼS S֭Z^ѷoזj)dXK'³`PlQ{o*O{z}hy]аGr[6'i(Q 3?MP$JP"`A`k?#[MP< IB#ܫ~+|RhgY/mz3[҆GіmML1۠jrA84(C!аG@2EL]ՙN[{uI%ILwu!)m4I/9:- &W2E䕺KZhk4o}^}I'3ŤWNPlQ[yr1qhPCa΅3puU&ӃzQEy( `uxf_mihWD I[[! @b [ˏNE&wYZY4df۠jw1 B84(CҡC<.d~9kH31F_LJ޸߉9=cW厵x_j- ~z:=GbLܤ{qh}ˮ}hأs&@h` e!b 6 EbF_dcz9?kz6auVwayI[),W78N7WC%%M9+9CAE iz 8C2 J LPSy|ڪixaLd6i,y}Oc Y64:$>W+/qm`Wng(r(bR$(0-*|*Aa|SӸ2PP7ѠUۉWH R84L=jqhPtCbLVǒu+Ft<3ea%c#Y9 b. "Mm&Zʰw> 3{dm*[LYx@ MC*]*`uF%1.X EFD`U_ !~K|$3|* $b orJ]kbVsWY5D"q:jgǾsuhP~84Q>蹅|QyFFU,5;| q#)E56{]xv_WfS[^]0Mm4dQP&ᭆSy5HH'PPlQsvdOOCÒ_ͷ {r(C`HèR)ԋ40N0ʯ%x<'8@)ftaMoYT覺գĒY?0=cN:`D6#& cР KCͲ}Cݎ3kTK,ALZ>(}"}e%ӶïiL=^[E2/6#%gw%3(ݥ?w$ cS0uzNаϕ'8"ǬHF$$qU^ԽSAX|VXA/LXqwv_)e`adt2u'(Aj% ehPr>4?ʹĒG,UGyoEn3:"BY{P%k-7KXqk$3‡g?U,SK?9$HTPlQ;gG츛4xơa+a!e8c$OQs O>y\ =3ԋv+E=Mk̆.4X$ 0m$fM-X Z&GF W$ c)РݥCI]d$a+Kq9|HQ[.n*M<5%Ųpa mڇ S2_f#1k= zh"@#ꫳn.u BEt2 mP+!xiŰ|Pݾ%akFYgk|`6 Ow#dX8&_ǚvX Y6q>g_G:̀[Ԟvf#~h0a6:0a~%XBьC2,jZ˟@x'Pbll^ԈFK0:!<ے#Xj*.p}Ve[G:%mP Nh0n f+6 t>4O. _eISؕېZp-<%DD%dA[ےkMD:&,L; ;W/jKavYMqwI-Fd@n*[Ԧ54 pBE x!qք, ơA% }{*nsR IpF=F+,*<QL8Ēg}]]Wlc I̧O?phtHP~Z5ЪLt286v\[ 6.Svb C߃N%=Ǣeh}Kqh8kar|*5 Ow $Zgu"|t }]P aE[O+N M"$ RŒHN'cGdPlQ{6?"4x ,ӿ1eX:de1AJ:6 ЌY+ƒzQEy( `u56ȷ;VkY4֑`~BI6'3:$#JI,gy4AkkNΌo!A>׾JV+2 ."|.NFgѠU{<7iWC lC=- M;gJ/=?;+?<{Ybҳ1oEZ^XĜo}q$oDX~$.!ϞAE cwܤ:>աAq!Рk(Q-I4}k_/Ϟ?! /yrYЛkSпAF_'|._YhW+~mw/9 )S2U}A娒T('b]nʺyﱣ>4}Ӯqhأ|sySNvV*״^8@RA@u|\2 ulrR00O~nK%Tvko˪/8,iRRXs2 UՎ4xаaD=:y5ą;Bt_W7}Fvn1Meoej:]W` :^&%3(]:3r C 8Qy 9'ԋNrNfyODMG:n b2*ைӰמNƎUkɠU:WOy}hP<ꡣ=>\bB쒾/%g#:^uaST7)AF:۠jw5݁’WϿz舡aOK/r4|%y. fcIɩkrD9[ԮAIq}4xO/Ї:bhkee"ڡWԋ^W{7辎vwJMHjx bE""!8*+9 y% gz+4XO'Xj硠U{N;=ia*`-R]Ʀ|{:ݑ,e_}rRZ:rm2L,_%湿87JkKXmUsXdΖߎoLW 2(kNFgѠآv<7iCrW44id.kw)\6\/)z+D0j8 @y3u}1e5}!usi;szPT=:ܤ{և÷44i2[E36ɫMwt)*W 旗f?5:h^YP^Ʒݩ<(`} ˒e"X;b/Bۏَx3ﱒqhz՜ㆆ=2x%a"UWFAFM&?G^a< [ə|Τ4υ_ &jN8C6ٱ{ S44iU2@3U #Dti_23,|]R:A,J"fx6T _Wjp6,` Qj_-`Tzb{һ7 LpРу=\Y0G^wO֪B67x/oIb Ljuas~fɅY,~_'^[ JG;'t$&dn`,A v4xCÔ14irˇ5cpd&0+M/ӄح="엸Vx{g WHxq_/җٖN:#WfPlQ^qwy=1 G {Zz#lJ ^O_/ ̬  !JwSAGŴ>#B•"{5+:'}̉A d v*\ơ:|{⠡a]ғ<$q}q_wG6*x$xL0ٳWl5sue[8.́yCfuBfyt@M^i_'Ԏ/8jn09 V|3âCi׋abGI\ hu5n0@D)>zqhz՜9n?k+*3KWW^1\.[lpY0)[D;u}}}a8PDN3Ge} ߂u2Ƕ+آj4S8@DV_;4UyyxWe4EvMn0:{y/.0P<^__GE(@,pƙ̫VY=X@ /?djzbTm}`؉S|Р K z+W; _(hwaR#0p"X^Ճ8(޷Q))^GApb!@%1x}!̠آv Nh84,%?ip,#E2>cJ=zr O<{%6' =W@\مB\'HPyr&I&UaJtPl9j5Рp]P-ґGe+3}Qn&Ym!DUE7|@=G"JYu2U<̠آvi Nh84ToO14Oy<ec&|n}hnY}_x|9k~ _WtxVBs ګ얜M_'saqXuPl̪d v,DJ}hу-뎂vz/Dױ,KԿ sur2elP w{E,\gV[^irr8 Nh5_0郆:)/ezѠރ!R,|r*_w3U gǶ@D5h_'SfbT(]@DVهe=t˳/|A䷌)uGAEgW"`u\~o_O˪g2"ˇ/cqpdѠآvW Nh84:=3i؂kxXT~o_G%q׋I,7~=8'/ \h(vWeŶ o_'sU}۠jS NԘX.M,s ƧxZ~9h_wm ƭ &!8% _&(XϏȼzDA Su_'TO^4@DV̍CUmԒ*׋Cd `u_.O &we;#ئL>>E=QR{/} H~\j6nH1'5F.J<4(w CCkc~V}Qn2YZ}O:|7.wYXY1X x<~t-j׉ ?`ލ&Nb'juh:=} I#֎wR ?p++{zvT›u2z]OhPlw9jpCCk-ϐ!YlSvYݫ뎂vz/D Kɯ Alqew_}̅}aAE >;9ъsזm8FYqK*h}hnm/A K [Y5{gf`q_j\[V۠j#Ŝ NnhXmkF(oQ\GkE_woJv@,_+^wOf.|VV^qۣ.Q,(u'5Zq54gyY*zѠ* K:"|ЈKЃ2WjNV4(Av F91gu7аڜ80U*yGueѶg/.>3-}g<:>{s6xsO'jXMM8g:^4hgR^u:SPVj$gEC꾆Grw'A Ncv؉Z<gIy** Xۻ U;l_'3آvSJC7!d vFt84C~FݫEvz/D {Fa6V\:pPl;9jxCCk gJ7 v<}QnYuVI{H{1:sO`PlQ;gNb'jnhhmnuŠɼD^4hZb:y1~8ardZkA vd vFb sR`~_D2]^_w_|Vz-|]w/} ,pk14+}"łb9|\u2;QXCCk5"|.y}hn>YuD}hpwh d6(pǧ6Q:؉V 746Oy>G OHMNC +/xœo&dӰYQ_'S;;=(}wg}`'599nݟr FIvR ?Nʤu2QZ}A 96_؉xFZS ڶS:m:YA v^d vFrhhE,z(hn6픾NfzVPlQ;gNb'jCC+bA}t#H_mu2t׳bT(@DVЊX}Qn݈mm)}6آv' Nh9VĢEvFL7 @uoD?B 27(O ,ż 7q2;Qc Kk~_w߫q]wyΡݙMw6ߩtΞl؉% nE,z^4hn G_oil6ӂ@_nKjwi B NhY7I؉ᜒ)dN"ɏtuZ8 ݜߍ 6@D Jwll `Q0WZe~Ks6^d vq8D`@v)&lC`\p]a{7gťwc q2;QáR;F 7v2傇 /埬^V48g?Nb'jsJNdb96C#WY=ZgՒgF`15$Qv2;Q#R@ }1[q9z 4frSK'Q;g0Nb'jsJNdb9F;܆4i\@xqJ\|l؉>mZ+ղٮ3-\vzY >9q)9"~I$ ^b5*} >[d vOV@v{lLF1* .35̺ lq]@DpNɉSL2'hB@_feHb* IeEj@\|l؉>mZ+ղٮk,V qipׅ N8甜Ln?$s$ ޏ]JKNd vOO̓H|2QKs6>d vq8D`@v)&G i~ RB\|p('5%x"d"f@R]\1'59%'SO1ɜD`?Icx4F9(A 1t6d x >9q)9"~I$Hûߥ94@D 'FI5퟈'#4dTlq}@Dp\2k&|Km@O\a v?.J?o~_ZѦ/?i]!%4;[}N5NZͿ:b[ n~ؐ$LmtAA:Q8/4rxVPcD]'64حiU,Tkmm tp%3ٙ f9\{ uM$ vkk zuvsO'j咓_n 0:JsSֺ&5͵:};N԰dp&;S48gko9.nMsbA;zDp\rMYFCipw`ZD`VWGPv'Ӊv•H->JwߩN04ؿ.0Ձy\q?qΗKHeMˡ{ 0:JcwLHFh%&PD F9% .3{`,p޾)9w'CilT'nMpnPޗWjbg t_TG 7|xOuNcT'ItN@ޛWGP'ӉQA@4J7?틿Kzl$OfOS`m&5͵:P;GzDp\r \z 8 FCi}Nu)' } zuU;Jq?jH SZ8JWg+ wߩN0em"i[\XЫw"v'Ӊ-h9" 66-GPcD'64حiU,Tkmm tp%3ٙ f3QLyI*M)@?:N8%' 8at8Q{ uM$ vkk zuUZ[kw?a-\ɤLv'Cipr)k]nIݚZł^w'Ӊ|'#j:.nMsbAj_kk{N5츅+48T7{( [N0em"i[\XЫN:Q8/4rxVPcD]'64حiU,Tkmm tp%=єhwRXs[&ao nPl$b];\%߾?7S۠-% ނ8'Ձy\t?qΗK@o37$`y_O?c#XZ˿?ۯs0:J-m7;݃?{w˫ŦO0 r{}^AvbU5O'js={]ϯ)  vj*tk;:ڤ]!Z=~_J1 }S;{ nP6"_~)WʙM^5q=S6Uf`WLЫw~D@kU /_ǿg.ZQ)2 1@ʴҾyi.iҔoTI0z %ɯcq@1R~(j>at8{x/h_Z??p)>8 uIŃJAj{E'ӉĂ EOX߮QG? '?ɗ񗾄X:[!/^1)-#v[n>֛3,&Hg!f?{ߦGOp`6hV{`} {:G@8RnMsbA6_ oڝO'j5Ei fؘ2+ˌْ\ aړ],4IԭKwF5Z/oߔ/^AngOE?wtOJߨ5@/@ވ#3hNZuMSM$ vkk zuUZ[kw?a-\i0=iؗwl:(92`d'"EPUbgFb{yP(gW~=SCfXӼ7>7{( ֡)KЯaiUq=St4ءQ<@=xg:8NԸ39 <k 5||ʳ;*0ڵns[x$jϮUMu<B{ :>>#.iqh V5}U=0@a?ݻ4x/wz~Ы#Q:D(V۠41,a<5+HQx`y~M<2HTm9F5 cCēpMrex|6)~7Op@60m~t<-'i78'Ձڊqm\5O'jĵ`/L^犖 "z)<lPe%]^R%^ŶQOiJ'!k @R # &黤(x6@X}1[0]Nj ?LЁ^1܆CI7N zuU;Jr?j Edf:A,ೢRC{\g)<4RjcQKV5ꉖĐh&A9v9ZQb(ӳNpu=p+FSsA='r|o9IYĉ>A2B975O'jxNG}.O13V^u0 -l$@`J/y84؃0DY# a~G+? [C|[# gmݪvSDk">Q6 t9JWz q ?_0$x)?lǽ@Wj`O'Ӊg f^_y pXVTA9"_3:4TcthHO=̊CGMDeO˟0:JG͜6Prz 0wܸYN9`V(/4|Cv.N5Xmh0Č˲pyʬNf[ siGطH;-Ssa#n=1aQ{OuDG=x].f(9dUUS#&I<꡹+:PxހD F9 5di0^^b`yA%~]ݎnUQCɤC0gFƒHKy[.iqȝ7lWzo| -ewA!8Icx^AՎ҅O'jD=/B ;Y?ZQ?;l |6Kbs? rx,)p,A$d/)1+(֪IlT&PQFE╬qϰ>{`[h`X ͿdՖ{D* IkWjʹ'ӉF"sd~b$lb2[{[SB 'S|o(QgKF>DJ-{L9m,DWj[N 'ӉgW F[ m%(36e4C ҜBby|תX*|\X&C$gO:at869 UrM  Բ Mӻzd}L9g:iO\UЫ#ڗۨD #hy0U'v[L|"ü"찂w6@==p[-MQÖ+$A jߪl^7ĩ%ZQC gf@Ӧ sz U,1=}CvV=J[z GO0yK*t zumt:Q# z_Ny̢ OP^e2Y+xMVo^칼OEĥPJJlD Pla!?yf3-SfS{pV{[FU '9!(zFCieh+6`` Z8dd-=BL9\e& ve?:&N5tBWd28 LYT'F`y7B4u-o?FY 2Ggu&ӮȇЂmՌ& Mym$f Cͷ@cLɅ܋^ J.cKKǫz<ǔnIݚZNptG4=5N5Nh0sYUVz \ q==LNeFԅOɫO2:.pO T1 ;/-i"pThN1BcX􊗯UbWQ{ {9,m4ط}.GwDۂ}Gt?qNd^NG-+2) LW\1[))t|:~ 凿r0g0jO_{ /yK| !teΌ'Ci>O[AnrtW4tZ(qh<8M(T zuNup?тF=`fVKWBHf?\Ca"dLͭʳSQ"j@bx˨nyV hU;fuLSYHYǺ2R[)k]nIݚZł^:V^sڝO'jIJ]h02yD0&Px'E&d}0X+ToZy훏>BY6i~7-!;Xrp( 6`hyɎH&Q7yQ 7M`ir{}^AvbU5O'j zD#:<cQb0 Z#eūbcadm.B)O$jmq.z'CijbhVЮeF=p$_vq9<03iLWNpt UǺ('Ӊw+Ax c5]ZH8Su &\zK5$SlpT4 [P|1ɟs~2Z('r,DWGP}U 'Ӊp-> Lc \I;[L2S-}k3kzd ۧiti$l'Cip5{v+$*M2O lBI/AԮ?4t?|v`XG6j~a|IYzH,>8~@/q-w(g{8ɪNKDw,8Z&P|fO},S.5O~`?p U(:QȌB~o+HRp"xnNkO `4.r8z3Dv ;m%@D nP\t6$yj M,}4S.5O~`?p wGQD4M$I0[r"Q H,1Xb)iV<8ɳ K;| (F  "P|`5(1Nk?L9jݤA P! t;CJ΅[7a pBh=3PJ!*xq3o;09J{5Tuo`ʥO4 zuvʕ9(:QÉQP# m{]udzwcˉcaTRquIt|AV zOLjVDIS>K4؏-\i++8NP~(< [kh&e ff? 凢U!ZB KD|K4lp+K֙IpS?o`ʥO4 zuv;}WvV'Ӊگ\ٚYOiuD`jפE lf-fLY9UB? Ý$ U8[?at8vp%`S{p=H=yOf$^Q ^SIZb^AվO'jaWZ*Je!~xpV,6Ok>#ZZO $B7>Vs{EIYT˧8&,9kp8Ds޾<ӒLR'Y>a}ӆd 7Z r.6DE K%RtK?ࠁFoԿr)G w4ءQ<@Y7; tt `jgNp^h^Bv_YzG_Jdi3 L2b@C_;U [d&FqG2%s:oR0Rw`hudɫcwnK7 7VD9A,Gݑ᮴T~b͡QSjt4ءQ<[tp?aA,hi0j~fz?/ݥL{ y*a! 1 9S*4f7))NɜMh@]v1=%BNp: rMG&E~a}FÖ`O0($ Er vv.䗭@EdYVLP`'FЫWAH'Ӊ ת4 V}%Q K|a)iaIgϗ#b" X( bڙo |c0%B*vBtzy3nJaCHaW<*PG l0N´ | :E98徣yfQn^*}kr)yI"N zuUۉWp? W/җdBU~ewg ɠ/Иia46J*T0S*3y} *9JըcS$s]8•t&0 3?l(}`!bm~ke6@Gȭ̖@gڣFl mz2]/o?)|Jro{)L' i˵ zu|.G5N5[SZ Q>F޴? K)sq :.饩"x_I3mxd0+8~ oTq"S$#.\Mg萢LNrD+~O,4N(,Ha NH^#"m+`%V\QXQҊue=rnUr8J$ f':j8NX+nki0s x/=t0Ҹ#[ ܲ@yT](iUB_FaEM9MmjTx2(32͞ri0{{9') yƂt*rH&) C@ו4Y?@F,9$ o'H<զkFmEe:NԸp- BJYmbӄN%GB4GeNJ3ЪF#O)<ԁ<(tdN B`ޔAii#i@2?8 NedA6}qo(%d{n'2̚>.nMsbAj_kk{N5츅+K~=F BiCEJ,DtH4Wj1I4I&ᔗE-ЇMgtCjH nVֹ_n e6dMC 2Pen0i- Y"-P>]$,MJ s^MU8  Xq n"tw5X_XkyF yoH`00Wof;RjޭdVL9[̤GWj9&N5™ϮL<-T,Q&焫טbaV,.jHѿU L9I03 UII2؅׶ }mF# ؟=w@oP deeZatTh{Qu Tjwr(iCxS#Tmo_ tJw9 C 1cU TBlzCbΞ8\M&eC%=`"dRP4$߾! 耱~Ou&7D%<& "69$K4GX-(J6u~2d)S.U'?i[$Ձ;}WhGQ tFm J,:1)KsweWLx5;[Qenj=SLVH^UOXb7 |_IR͂^Y]tʝO'jD\: *14msqe#8/PϩYE!(H2إ$SPO%HܒpѪL1C=N+o=j,Q1VѓYb# 0^.7H.'rr/G{3=^AvfEuO'j,N9#,Л n`#n\!l-t(lV*2XK_bw؆$ x2ܬFtw@njGpB,=W#Q]VָX{'Oan"VT4Ru{)L' i˵ zuv-8R tF,5i`GZxL2 m,}~qfMMX*LȼKK34G۲f vWIΒaq,ۋW<AbX( +xUWifsT0:i)9n'%bO ˭ . l1"NĮNl{?a>tT4XR[:w4ؕ9(8Nб }T]VoB)oKdNBx@O-%RH T_kka.jW8ɢA Oe9r5`Y/mJz $ޥ wO^OtњfVظn<~feGq>DXNk?OԾ64حiUGwDQ[|SD;4\/PJߺգӄ3i3[F͗l^I$-nD%Kꍈ7 9at0`nPLR |-E@P,2̰)) , m<ie#N5(Gn *2;*3XGqb,-,G*l؝G6C=GL9n`FRЫWKO'jܥQ: r%T1DD@4y`_y+߬@Qf؆$LC`.T4,->if8'M4~Â6K%1 zBzc4-qb4UH =JNS?p1V:J`Qw;4^A`qN5,-`>R_{ SJHB M(Bpi4'zH"~5Z<ήip CȼNWǨ,D7ev}$c=Wϔ.K}Sz84؃@]%'ӉѭNO32#`6epW8u6.TȪ.5R ݣ0"4rȈxT)9; ZsP$0X#*h)NpF,1(w7f-B%ci빳 c=Wl'RѤPWjowOO'jQ4%?LûH@""6E_ףܣ)&ӈRSQ9%j(_ȪLMȈKIѹ 3+P_a%}ِ8;Q-Kaщ:QW=& vh*:O'jX ZHL{ܟ2ap`fw #,di5@jy9  }"W?q(IVf(īLnJXȭ|/ui0کl#;Eߎzԩ>xq[/]=H|$H'l,i&Wjro:Nh/vQV ]rN0e=4Px zuvO'j咍4XfP Ѐ4X~o3Cʔen$"=3*˚*6(ۜiѤFXhcFs#ք%LFlj;Ss͆:ÊV'rU $ >zuUgj:Qci{drrLT̳y֬NX?KW`8ԥգ(JHE>cD"Lʔc}PGyٺ*7Ji '$N៵-KNO4V]Lz WL^{L9[N`oqOЫ{z'pD W髌3%${Gsq) xj$sԺH;Dwy ̛}zy*Ye~^R-` @e5sn:t`"`uc&W1S6oV{TD|ř:@olI/'8#@P! t;i[~68 ӞD j! OI$ _Q9xQa5K=@x>Fd"Fa%(+R/GсBc nV ]=dl`%=_6Tr-q 62޷(z`Qw;4^O<]'ӉѭoL˼+?_m{Gh0 \>&8"(r#MLC oDU-90., U"jtHyQぇ ۲fe5ww4Pso 0uwC?1@S6Ie <̙X 6 `%9^H|kVL1Ֆ"7kňMfbKr]-mxSzTq)k[&5͵:P>r-kw?тF4~˜0dKOy(ݝ|Q.DBͶ!Nж.,k&!ZdgJqlrl SKP~X/ MɴU8v|?<R'FTB%e.n{G4XXt,)P>?w ϐ3̥/^QxպNpcK/iˌH.w(?1_ fM${@:?Nka(8Ago'(՝`vL~ zu Q'Ӊ-hB= -9g4'[J1І%> pZs0:$S&ɆK|drb1a kz4WI{=f ǮKF# v̚xjD}H6֊)/ m掏QֳJ-'r>rVAj_nnN5E,f ؝ueLܲNv7y~ ɱ9}TjZ- ѳPk6h}N;i[0P~!1佣4KYMJ:9p d$Yo p7iCxP)ՁuP#l\)7Fx?Ԇ{NԨ tiwi8ST pwXx> %F *k9#mro!(p≜iF87(QͲCG_lPZY,b|; N Ä%1g-=ԔO(Ƴ:-B lj%_J^ho=wovY5п{یInQaKʅpPg28/^%b`> Bhb\% ]yۚY-%tÁ lFQzhGi'4$9'O|>!j zupNc?:PC?{o̪IϤiVy3; zͱ)cYuښXgaZ65(o/}JoGғ:sP( E*;ǫ8Ti`Ҟb6{"iΔExN a`"r NTJgW*^ /`[sw?0P2ǫ$pFVxlp`ND Dd sEKUv"b~Д`}VL;mc?ʄ~'47;͝b1Ar@q_pwcν%@Vy^?g C$ ș|ǩA<099RsKJMkx)kQDĂ!}(:)KE'ŭ$w /h b` q[&)&m*ͬsST7]rqaȂZ`#ڒr!l/T3mz/ꀚOˎ>S]z;aܿF*FLN#!O̯U3LHoizc';=KqASr n\`4_e'wqQEvܕxo(wiށGvʷ1ݟ^vj+C2A;=XYe:fɏ@`@Ss"7g\l饑MbbC&CZNgՕvdK~ |񶶾|[6R?$&ZHosdk*(.w68=:tJo ۵;׏OuiTe'/0v;UO >d堁]=#6LCN׶(nHKbZi.XCσ;n逦Lgz{ӥw spj؉yti`g4!c7iOU4{0~%' -uKFzϠ'aM*l=9-œhࡄQiy' 3 *X:F&:YNϧ3鎛Nv59t,7ehJ\Z0& q71&vc/]3ͮ>;H> WWTҋ^" Gcmr._nU$G1RiBeG46CV' QqN R{[V[D[I]_W֢ /]螫b{qtaM:t=SfY{XH5>NfehKtoQ/TJG*x{Wgx)&q t34F'}*6q0xۈN FZ<"ȉ'荾帷3{?;qf XTb;qx?ͩ=c_t㯊Tż8[ Mv=C7b$%-<|f_#[~wCK"NFmF).sNK)U*wO#pX\&c@`=gQ;wzNϧ I0i@lM4Ǐ`~}IpjTTż8[ILg47auc!a/Θ}={4{y;/J{+Yb4\~*ƹݢpx8:y^IG> `݂AX H+4` X]UIG ^'i&v!/[ǫ urN^=5 vZ~h<8弣Oō:pu-D2s b0{6ãA|s U)<078d堁I{b۽qNIq*GU%&*Uĥ|8!% ,1oʇS@5iOIyL?"9$m];^c\$:G}Z)S;QNwYdw7m>WߗWܰyo ;y5;Д~"a>ۋ>bWФ=eWݩaiU]if鉧-$”\+A9LHYc@bKhG.&!I{ʮ;NV%)7Hs94Os b@@x >g.gR^Ι[pG4G66]>}u b9P>pn;G%0!|~0\UU1)y OQ w4xq@\,Ik;C"~7r0fy_*b$5GNi}YdE.J# Fv}_H|44oYDҜ]: ~7$|5Nܸ㖞VqNo4ҊR<ςy9ή߸wroPRj2Vj[ԊA+ | YH[髈RNT,&b{@:јT mΈUa鯊Ĥ_żTÿ|[?. 1tKs+42yy1qԗ:Uݵ)m%bxe?F@~$Zq6q@D\pS! a!WE2h_{=m wg.4Nv#9>mDL1-΂٢dV"Gn-į Ƃ#62g) 26 $ Q-1QH# bgwq^=c󵵓Myr [DI{Gsm<' Sȼ$FLR_^b+QAQi=ZL;oٷaJ=ԔO)ԧ$xG,Fⱐx$U@cI: 8˲r~|9yi_f!lކ=)-V^ã 0FC~v2)+w v'}zOhLw#% 3SsZl:|9IBr0}>7UQBq[m(&11iO9V$ͱ>?":5Ͱ٧4O8*ѧF aEkAPҿ{cq1Q9uy$}@w`6+M>|@{\7J`N )x`@!<+qj(0dP%Ad'2)wu7l;4ih.s4 &3c% -'/Wsy3GeejM:{ 64`N,&^%0 vUaP0ɻ#3ȃNp@SzɥowcށإO1ۗx xr4ƾofԥx4yq$,tWX77 ?Ed۝NSv؝zjW6#w"=}3/_A68D76L c"H&`>B<PH BSY80)\mP!FW =zstG0] R9&lzMd ?Qw.z\lU? NXgc;jjS#d07$P'NĀ9XJTVsVapfۜ1 W}Zc2fOK(ؿ!->% )<7g& ~;aFww q]Uhwg FFbwhʆvb`p'vށ)y@ fdc :y]@.~I|`HJLR,sD}<5I{]szjJ95xe^7{[ʫHz~#X3iBJȷcs`N,]t6M$mT=My]Qw.0(g0L+]%AfdaI4>> mn.N: &g5ߖ_^Yv)<~UY wH RSpQR^:S8U{nblֱѬ_S;)<ӯ.Zb$PMꀦ$yC 7TGb5i@솎ԉytɃ4\3&=02ކ8pQD%Wǂ9 s672`zACxcpbKugާ3y3sˤ=%=p=@2r!N1H2i^vG5޵f8XҧOFrz#9Q Ɨ7-)8j̞(h$Ϛ0 @i†Fڰ/^ڝ_|Ok`AN>eh2{iZV椽>;@:;ӧxXd*YED*HyQU̫k%?\;2/7MYvXqp  юQI{J?p}v~  N>+OLĠb{zԢa]_HSw."efAQX'JA)|Z,dwu@nM@ʃNi)6ʌ74i hʴ"L/|vyTb;@i7Ƨfj911A&9.iN-5I384a =꛻ M)c4{0k\ap0 -}IdmP*6VgR9IxH$XiYS) fIo {ASVl)IYv!/CK gղ2'٪U :1.]abrOg1v0D՜UHo_l@ŀLwPJ\i1N9xKczҞrH7AESlID /*ZPNwvWw;- XxKic(faK̔?rl(4_܏o;MC9X<ۏQhJ 4KL*EpHos$lކ,*a];).X%) ?E/tށmm<`Lx$h2SEl^b^E-d#pv7+ !*ڳ=-l'`Z*N)qԤSfoKUPCB*lźjZ"F~/97gs9? yߩWYʬr4eH )Jw4tzɉ4Y4ỏ*x3Qż)@J$ÃySucf$VXP Lhnr"Ԛ=85x8;k^?Eh31@vX:V; /{(3h,9dhe{4)5ғ9=0 oJU[~?)kEO`xOQ⤽>'̉4dIS8mJ&TE\bRʯb^E\*g~[ӟfP 鏭&D~x-"I;} ')n4{r`r^4{[ C9ZyJQw*Eplqkg]LM""HV!"`6 $cN|Ӱ+GG{I-$id4&@Tŭ,JMY%S ~_褽 ';1.1Eŷ1{:vU.gyS7xW<]@”;@0eN')ZN=f;6Ezl>0{H!yiw_< "S1-;ரIwsryROpCA=>9OPwOGBh y' p0 g"3ߘxN4`](n J=2ih.qj4&"-8LO!uOX2t<)Ӱpx$lֲSBE?Y#;(H\C<\n!ɶ9h1=P>4?g53iO%f:^`B[닽5oQ @ś啶~CRj&1]my4+ wgaA8 1}D#6𸃯S|NosIXkCx^;GN4ϤZ0ڿqٓ.!kc^r`M|c$eeTE H!H+8vzTh~'VTMş;}QMP`|͎d=t%8cYvۑod5B#rؼӂoLR|p8ĸ0*^EAHJ` h>(y"&ž`\%c,$UEe-]NSY #O$^]l s{ox^Fmݼe4Y"s?n4e/|$bZwɞ`@AVIbG*Y 𾼭&g7m;UiY[ J,l)vhGP:-HRWOqQJ\bXo+KW瀔w!`Nl~ z_Jfye <a@St`ށاMoJˬ.}$bTkfEebHr7o" @$:"͙U8okGQ^䶈 N >LW|o +a"G~BxP$@ jWv`N9#0 v* 4/ hcwY0n-ry&ž-(t\DGuE1i\FZq^1NYV%Ijo"fE LRJ(@H+WHpjDyGIQ8[˷'xtɕuLGr~|v ,gaof} yؼ`(V0hknD>2s<hs(]Д`}VL;w .O ̀l+IJô@>UsTII,A|IPGTf+3')Qlc̣̪B65 X$*lV q9WeC@ bA ]WDؼo!! 7_i N Lo4_gQ.,߼I{GsoJx$ $h[_VN삞"(ojI$ьnlEb&c5&UƶKjKh^[ ^˒7^VX Κ]I:y/Tr)M;v}׬TGh3~OĎV`0Ty:@_&efpQcrnkϮ7\7XjjA^: LXJcZ-4|WĖ6o]`8DGwViێO4л,|Kt pdit`NOmxKm@@/R5 ~Vh+i*yB`S~B t=m ~ՙΧ xAx",24X8Ͳjk08q.Q'z|1 vd?$86F!E njfaxHB޷o)͍f*LS~ioPنim5DO.\x'1)=bIhxm˅?4"9̾ %${G .1G9N Hx_^y-a`\j˵U`mA )MyMw-%إ;b']gOksjXc/Jy6ñc8J~ ⡆_$`ե9s{? ДW7{kxRfoBvEPOc(oI' BOqh(c#ͼUUpJ55ľrwlyҞbd:U 8NdoOp<ͥf*2cJ㝢H Z~8) ҂7i1ih.>Uy_+I0|#oXueM3aw7r ( bBގLCsIngJ sk%Alb 4!~ 1Jw!~PG *)D\;+Q Map`~+cA-x'zk+7`F]w:m;Yۆl n̏jށحz;E01/wy4k$fRT=qk% 6vcMw]U[% zl,/_g5G9uIғ]-vw0H: -&!Ơџ"W/HUKM`0x>oCAU|W6{8ȗ9pcW Д}̽ tL;}<ȗWf4a4 |fտa~K'zP`?\@c]3i\Zn%XE *Ig`-6( L8/ڗg~\Of=p+%x$XmN}G<`6O|Wqȟj}JI{b V`fdZTHpjɔ Jds aw Hpozu-YH*bnK%;#~jU iI{ʱ"i>`ۧr߾4(~DQ׵%q? E 0a->4K,p@Sz*\ۖ>ih.txTц:]eFZfMB-8I*UI$(IRƷoP_͡ō:wU }gJKUw9cL0,'9o<%`]aPopyD=s$g2ⶉo4lށؠhgD2QȶV VNIvTl$(?ҌRә]Wl2춙{,s+Q1C67Tũy:%hhv )Jw4{:Q,rԶ7&\Y aϿ¨A *UæVvbe\!R:(a#2DIVsKx?-9wEހK[{,G`yqwwIY, nЏnށmr0_^vkqBh3(Kwu XJ1F*bʪDġNJ9D=ḀI{]ޮ|#"v!–Oq 1 X`"ɘa3s nOQh9hrre໴䘴w4{:Q4(8-i.H] >{PA-Rs]Gb 4vx/`Ӱ>&x۳l+ .ZRTXzy[Io Д״:Gj{&Ş űR1.h,roMbsk0#lM\!Th.aL!\ kô:}VOɤ(q 3TCHx3Ofm͇٣]a |Ɠ(aG-p@Stro(wiށازJ6 cԶI/̸C/_UP!J뭅X:`!CoCC9-a*ZITw\gwP ~O!Ӟ4') q6*;{ޖmUDGź03ޮ"eRoC9Q/{,5$?jXˀc`#N;m`@ )Ϲg4?<7rJYYcfbj$MNZn/=jkS-aoJvqSѳ=2Ȃ*3jڊ(h8(1Eϱ>5ڴ׷T):{/cS絍)9l;遁 X&( !,`ꏹw1zI{b34ԶL K.`Y<5@@s籂]'yc(D1Tb-oƑyASnk%S:Ç/_+0i!:B9iOQ}KV}zɶTIg l.zB)=n`I?A.X/)?XVW 64e ls0&v0˚ }'xJHJSg4P. prK#q#O]ͺBkj"*I\9k!+P7B7vjnrي t}ɶ|Q҂Q-`!܎y:,CСΌ)9l=8= WFHxMzsX0'?i@wLa ö ~1;G*3(=1Qr )BXbsCrOYU[%ɮҩ_PXB#vlbҞKaƥ`0᱄ZaZ:r@;q+ 0!%kا5{[d .Pդاx!>Xm, 2bwj[]_?3g*b/4;Nxyap PiNa`0=lFۊFٛrw.3-rd8)k<~:I{bMl<@}q6 Þ} {>GUOwY!UްK~%yՁySّbܧ6#.Y ^fw~8)c%I .iw?ih.`;eM0H؏(a#lY`"d@>0OhCVEߏVU[@'H"'&l?źX]Թ%w]u?`]ap!WEyAB5UM4eTm ߶i+ؤ>x@˼OTF?226lFa > s*~ĵdrQol?EpdҞro2l7Ҕ0WBĵ6N`|V7zdwm5*З*h!g1fF`)N;$m$|E TB n sH0Rzpnߏ.:G|A֞S|dhPN xhnr"80%ُQbDMoAP/:w}Kl+ {Y%yg6t*h |˂>KqĎ#FU;q-a˅ a'`c^Xpɲ(._:VMbN"q4~y1b8|(юļ+/el]bKXw} `_a;H8X6#| PXR,p@S^Ȼ ʚw4ۉzI3g`l&{}!B)iHU}?bDbV!R &Vk^4# 2k`Q]apo0FS~J]gr݈u0+wts L;) fa4΁J ܒjrhfPJ"]D~I~`? WjAmJ$Q[<(_EO a>-NI89ĽY.(0lnrFY.UzhN rxOQW$ s kq D +Yk ,,GqR#vuv[qiGsXP? <|% nr~'ys6K$QS|\NߙoluwB1QM å67X X%4e5ڨJn*>*:GOnOqD$iLgx@qԮ6>+Q:4$٤/ Vbۍkh0ԥI{]/g ά5{[aWSVm `]apl%їw4<:otuMVp ,6҃[JJ<ΛY3LÀ~<~#W;5Զ:OPzb o N IS%I?P&WSx/Z]7^u[elcw4\Jg2U üSmTTlxR#v'}ge]o 8 rjX#C-Mo[UU;~m[#6$"O$:$ 9WNS .]-ȫU6Rg5sĀVg2ό GOInnNb-ǣ00$ܗxBy'LLJog/Mm|K=6=)*5 j2ڌ 3i#~҃{G+"v'}gc"wNo oŃd4ոS U5EגU0q՗ |-;򱾓cEg ΜW[`]ap9wCwMg2)PrZZTM)=:b.꼹n 襳5_P xl_(zaBv|#vp8d_obٱ ΌHUWäJ,qÓ%&$>l?1 ) M.nU]41Rm? 04VܳYZL:ŒIl!GxWhZ~-կ"qTm!t[\zcx {%a&,>lT\yP<|eG }1Ė*b$G _v#oҞbc%öi+c\`]ap9b@V wU hTLZ<%=Rǻ:Λ춂li+ 0p f'()U@xUbvo?QE$)gWܜܕlnr;O9uxWgU\ Ӹ,p@S9Bl0֘)-:ޭIuNTzNl?mi%T/UVqי(1O\nqڤ=V(4sFlwxuwMɏ2UԌ ni}ݷIu\l@,Fp#Зx`s9e|{啯$!O !E:9W;yFsBi(L6䲥 δްK~\U_l+ oZMTU-U%G%TI\ xc!07bu3>Tw4? #iN |fo,wD:va)Jx,6OSq6{êO/ޖp]}|WvxbC<^L\KObݖp@~<̋[!Nfh\)Z0:mC$WgoM1[rƟ䌊 daէf?Z 0XY4e5ڨJn*>*:GOv%> T9pl" FG쀈38/_/({,qWIL)ӣ@0!qjpҤ=ŮmU^-D 0Dmw-z@S%FUzpKUQ'ysnˢ<ri@d>H+[Fx[5Γ _eU"* RA!mUى&DAraFuQB\VɃ3m7Wy 78eXKOsnow[j` ; T!テuk8,1G']mPl0(P현/_wI{ʮS >7H>`]ap9>rAMJTU-U%G%TŶ-$ x@{((b{XE|mGPb.DiWΘMn'SNm7RUGl+ ^2E,LkpKMOs>-UmzxE+qL|s*6%! ſ EdXԘ .mFh#〦L6zw@RHB4+LNg؏oy&FoדIu\lg%׎ݖcC[Rrj[eE?4OH)*bOzm߈MsH;1^qt*VŃLM.r~biy._E`]a=\u`W4e; ,q8҃KUz`sZis 9$[*)TImetQ;#6<\ S8nR:R0 1=0)@̏UT %I`6C Z16ܨ¤=Ũfowmj`TWXπLɚ?fF" 5 axaxyMK)2tGnmzR7m9bNQ;GWOtX"E>sn\ __ hU g"- Jk?U)P;ѡ\UR`,EOL80%?S y'u hTwX@ђ'fW3e0_g\Tz;Y,JSzpKLs΢-鶠&: O4x(,)l`ƣ(ϕl#V$+[w=\ϭ^s08[ Jk4.`0M#['U Ф_6g3#!&)vmGcov USt̮?X~4q*lO .5wGDVC(]L)4)or9+=@꼹nsz:`5m s %HI8ѓ>eN[!̯@=·slЯ-ps[W&Հs>5%| L ?sPE,06+xxzb~ |dK&C.MsĮ~=tP&Q-5b`x\[eZEMJUC{|"eGUXhNviq;U>&I{NEti`i`i<1ii_^nS6Q*b&8}7D&8F{[L.Mb;-Ix4ű?%qGg $Hy6YȡK(w»rzgЋS! ۉعb3y9Y$#^%ɕ ZKKKSk9f@^Iyĩ> _ 1KJ$5quR#)nR #Zm`Zz "UѸ jy96IL'&̫Q$!@x ]z@&i`©kLSUvݵ444pYwKgQ Q*b5MV%t;dߘD*{˚hj9Λ6.e{ۂ yO,rmzmU_7|zDd 2Ey|OnlNbVy,iE"p9DI8=}@TFONshӀ>NFf|f!"wT媟[c?f1܊ 4%FPNK{qb<]&=@Hzn -!L(#z^ݓp}41bتB܅<&_Ei%XhW&)jnYXXxDwBɂ^1jTE|| 1Rc꼹n,9z}1i2 JmQE,&p ضv(izNK&C9.Ms.Nz(}I@wA_YǎSiY9]Av(‘g|L>^:攈ɏx8ޛ͡YGJbA(J, , , р=rI9i8S8=ٵ"WsS?&l &欉&bXbR7coHn[₝qÖH$ ``,zgxkdܾlDDM.r^{>j@#& I3. t<0zEQN̫k1:GlVYv=WrK,M\vK2FxDLgEn0r`pSlg"0:d')+jqXXXҀ=NMxě$DM( RxI95a;DchӥIu\l;?ۺۘNM 96lA7rpRct^ 44^i#I f8I J"zA]eʦ2iI@q -;^\Z,&KKKwӀg>e"BQӫ>+(" N b.I 6`̋$>3E*QrLyM4{\TmEy7Kn[:n^B,'b0m=K [!m4 ǩq9>)`94ɑgQ+I{Ju-&KKK~ Χ `xd> /',s4ȓ ے"fvW#DO>#x7G̹Yr촫DcӥIu\nkݥ0NDWKOذˁ']5/_񂒦-En1$a%A`6赃1kt2K>/1մy679uui`i`i`R xS&&3a4d%vshzz:=_^qh8l3d8gސ{1KSJ-3G`Nf~M4%˟T;ymn{qTÊv084ꍉ] NJcݧe}GL%\]srkTRWD% d%C(x@rKf=hJ?Λmt[W^=D.`2oOSeaKK%8 c@(qx<'}"C6dnmnrvqS2c+oΤ"6gdV+1!fYOu)3QA䠖D&fOsn n{(㤻N#eL\붞LJIDOV# 2q/ kIKK6q M`vRҀSIc5?ebϧmr~5%8ND|hJr`*5hdɴ%9dAy:S栶꼹v>sZO&WT-ϭS(U!-_5U48-"R6?!L컚]ܺ444p+ 0?4g9| #4藹x4s)YɹA}$f:sArMaadnϒ)S H/&Ƙ:Gljm@F/ꕻUӄe!Q `^iD?ZJ)DkCaѧ<gI oKQ> K1iOqjc- , , `f=?{|3 &Vaq:T!oA gڃT֙I\zq4gz`ŖOtEMI1h9ΛG@ȴ;uXi|UqJZ Ċg;*p1X΅ǥM9P./ka`sKY饁Y4@tXxX;\W0qh@&LtΧ"BfI#%hw#!Yzm[fg?k٩z:GxK}wv۪ N&k& 2?eԅQy('eNw0]$1$eHIjn%<n*6 xsMv^N&9brKnO%}Z`VshʻIOˬ= f ~n&/ [)%UXX0VjSg՛qR)ΤʬOèl4t _n4dg54QoIu\lg5gRU=mCDS!@ZRai. f@1r˱]\s[WgԒA 0K2z3b6+|`_oi2%?Ι8Q;i(H`pT~1<-(_<5DU KLs}z kIEE1|66|w8k vCD}iohH1Y>M2z>Y6 4bbh``C9G9z`0.!Я{m*ᝄ.޵ [aMs%OȬ ,US2'ܛF4.^` R$K7/9G+afw9$G ]#~oys3Z&5uix_l{'8=OqohMA' t8?V j;b;Ksv[1P<Vz` ,( K?Κd't9+.m5fj%Jc6ٍz-jҞD] KO 8S*GFc˜UFx-h.ڐ*;h } n]S]&k9wΤ:o.;{1ֈ}Hlw^gm-!iۖö'i81'=›z43IXncRI␓]K s[WgԒs]֧m1ض>{?m/H߰Ǘdy=A:l5v1"iwJ!$X]]-is ,\SM̪b;6O51S`vt T Qpzb'D,`&ܛmw[\oWz#u^hbecHpR9 4)'8$M)" y-kO\ɼ |^~#1Q5 $C &;%SDy 08^MTѰĤ:GQl+uwB,;r.>e"  8H9^´*IDs0YD&K}MbݖI;EcaoCA)2c£zJRK_%X3|5 G)&2_y5`m${>MU>%n8ߧ<(܁)y"JT1_MI'9b IK:K=۞64LI5`3w9Н*r3JjLJRK jemsSMY/1-zҞVa{h`Z]<րSqWͳ>9#s4% vIdZ#?dDkW ~k&Ʈ꼹v8uU=W˄?Ti?ݥlmG5 VZo 7hդY;w|fg;`\z1 pg#B;1 #l0m<ˁ\_dni;9UQdd8ʊWTTZVIZ-vѣ(~5'$8lnrvsqt  JiO hu.a%}iZkkѡHi,bw}EbYAu5ާMۣxMVi$e^bC*VIZug}XBb& ).D  >Bsh lΧ(N?iӬx`9 bJaCf Ez2yw=n3Ma+]X8 HH [@`bs ^HJ_ltG%CcTcw!vS-c0|嘤ܡ8.*`rIK̻MiPS9#!jgrL &!8t9bovnK:ʝ잵Be}k鬒V f$< .\5jZwT4mݝ{e־WӇ{I?Tc\GQ_ ?|<=`QGKSJ0h?DdNw bn;Jd.o-<'EZXt v4>PɰUЯVi`$L!#v79s>۳pٟhE=AQHk sL81kSK0lAzp:[GT_kv=F]e:Nza3SN9~m՛5ݝ8W* nE  z'k / ,w\KRk`9 _@68PAywm^)gKyb/]R.1Hdfjy4V#57IM1˫m.M{jM&C8>ۿpl]%moꥁ́ &2u.~%6tXx FeVea3Df^65TQ%6&q8Ƀ sľK4Xwl_bI[> hEfpXh,#^$ >t"4 ڭ )v6g7VvGЀ=p,!#]^#Dg UYhҔKTSR1(* ^#>nvywm^q}Z{1iš2âs:Xh>p9vkUl=-r70Hx[([-M_ tfV|* L+Eé{码yӤfJ"ҿZϑ: NP'`Cb׀>n2턄yL'~>#Ҹ3hE ${OT^s VIIB V{~)g,Ȋ")"[xBlEL :;hЀ=_.~Qi`1$\G~mAźN mrSs*o I/G-l%y[5i!b‹nw[ˮVYdAEѨKڼ8K΁ǩ<\l{F'DEr$HتH0gR!S$G00l  thP#v_t[pXA& 1)4x%t46"6g해+: >9 Fo­0!7C+_wAv)$i~ޤ^ MptǙe:.myz_nR)]V4kC4G%sC:vV՚C c@M1\iy5m F[:K 84O-0diXRx^Z?nv9b:`<~'n p[!Ui6ujguʜPOlDӌiFŜeEe-j}nE3GM1[iN_gY-m=Yu> ,LLl$ ̳ dXLE{tkqh:.m$ 4&L@6nR61O9`Ovsѿ$n7Ż@7<"S_Orwsp ̞h|B~5swR0\k9| W~IiXւ5a8z~8]5B<鶠;oIׅؐk`Iz_Njg51Ok%͜^J¿sM=)ʃzFٯz Gj6WEB + c^X/3WsSJ/L ONzQQi 3sСAu]lۦ[{F@hA4'srko鞴[ZYKh 5j\3fowЙՙ{Wm]Wg楃 )yzx9s`ɯɬըYC@WKn8_6`a ˪80%bmL"RȘM+g 86~=}Sl>۳0zoG/i"ޥydg0lmXˠ70O- |]oxlU, ]V;bPw8&kuŚsܶhݼ>3GCH5yh:.*]58m :?KsTfxŐ%C99]9y,궻[9rӐfD?Y%m~ K D.DAd4GrhJvd׾ g [B0iLD(I Sd8ZG s6 i6ݖU,YnKAVv‡kUM_ѵܷV|O&F(ǕN45'LOLlSlo}ͪ7~82I]t`qt ]̧|%m>. i9`i#ȓ MziXKg*4g3?槇Qj+bfٚ?:.mAM,2̈́?}J+n{ y8O@š]s2 Vs \;_$,u79[!uܞecla"a{q 'T&@ 9&2w4M+vڡ[,YȂ6cQ Igv=Olڧ#$'ג"a0=\y8 s\;035ٳqK}\_e' ДkZΠRKz>kff`Mttk85)|(PYMYkO'kA0?$\ F3t[%m~(jiy 0 Lk'Vk)7xk5{B978Q%?H%C.E j9rPwLizqc61 nB`$fwhz bmiMX" ȷTFPɕ:iy97 ]KM ~b(~a%kW/5ԧI`2WFׇ<6X] ?bM̭Z .QRBFK@)j%mPN(m˾h`,)3P8_2 thP#v.]4q[`3H#n)nۥk&(FOkt6Gt9[p $ G 3le(rم[MFC2¦?~M4Чn改Һ?98FMɅAg,|4dbq\FKPioi$a*{ ÷f4( thPwBa&G6|3.+S$3&@e_!b=iB9*&[7whM,FbFׇkXu%}'K )f^^fO5bpΧ;ҤR# Aat'-ZNJ)pcXptjNWSqkC>]yq'(LHa 5>musbhPҤ\ F^sh@)b}:ir<uG5]4{>PH׿4D< s AQܙ:n(ux}]1i S-UsEp[|I7ziKkG|{ȓ v{I38 @_kQsJJ= I8]Mb;6=R|K1˼c@'8r Rkʋ1AkjK%nu?}.ޡ е #^d䊭\4{ükfiYfL 6QdÓSZ֬i".rUMy 7(ƙz4Oil6o7gGR<_ % ߧ')ShZI[p:CT爍uԉmG,#A50@tb$ hB rM]ăb_r =h#Tb`!c/J˅&|2 1e$ln wS.iF < 0*@;D&wt@X4Ws fի8& hЁ~*%_P1APk-2jd$F~3@ӝ{rw'a߳foSH)ga`?q%[N孩;([鵗&b⡬^)c+e/efb0C4X l7 Ϳ&gvA];4{aQ"z}Xo xIl6g7jk/DlH>nI6:t{>P.ej0zD \sp ShUb88%6TNatڏ8:۾jՕmӤ:ߊsܶ6=}HNU襞JS3ΥilQ5R3A=%_,80! "7mO يX '#bcs3@! `K+qQJu3MߺcYy:+~C%\l滗y|>ԯl~!Í&ppNl`3λt[KLܴfu4\I.np!&HN|k (F.}JǣsaqdMcvrڪ7kf`Z c1O8JJ!涋P?6sfگ4Zի90fS/(w3\qhtA|I7zi9`۸'Fʸ18֎Z͞-yjAE1M<Ƞ:G\OSNe>by5b MYMa1|WPvmjz4Nd\#DEf:=Qa)Zf_4loV0 d&^9׀b??}8Tc.L'ËKqE^:2eĮA &U!%p`ߴ^ ġ^p,>6PP^KS_yh8]prqSɹ Sx\:.m;-wǙ>zb5k%(H!)$U7-MSduK%WqF5=|:LM^:iy^C|^Qci?" ]XB6ꛈmVţG` T=cK>czW{i9$] vc`i S~sz@8]~\T.͘3&t:4۞Zt)r-:*rs%z^ΰoҨ5ixrjKӼ5?( kS"uncPO;4{ük̞ǽ1SVD5r0B.tZGwV>$7*bI?^["?@}S!yKq 9p[[0/0 s*K M ?rPw8&@ĬoaUyg̕ΰҢU7/kldM9x~)Mn-5~7,xo7$mn fbS€4R =ظֵSIu"rDlՀ`r P 8lVV sįPH].inFMw <ҽSsz\x0l/gĦS)pʋ@fw*yӰxCJ)9dz?3+}^# ,u.1f˷j.&|9b Yrm9Fq3P=!!։GEu;F}X{%=VqEZA=ؗ\iaۇfs; xHf A W*]l,Ѝ|x/N6UcsĶVnkxGpVm4 0t 51?8Д'Y|h2ºf?*ρ Xn&(LJt9|$xz)3&|ywm5r#vS2jJ ZtkYAK`,{˭B*?(y HlFSiZ|B=(~Vpڱ#)K[ͫ,e~Ep5~}qRE*/iFP/ `0F(`TnN<| e^JppT爍)z:褱5Gp[vDv6qiΰ)n2fЁV1P:藔r%ͻG1 +EVNzT|-Cu) qzm`S`&sNPӯ> ȧɨqS' ?=gtоK|MQQQ 9MwTԚU8Z'G bn[`ܜѼ8ϐybnr~Q+I;ѱ]w|\%1^dӳK.0gfVf/:LyDV1'T`Z,(j7<<'cN(_}#499F b>"*@7ozJhwcT]ŵlqtP#v-OKnpFnw$F+[a1)fy{Zפ@@F TlwJxfz GkUN1ҢMEk敐iQe`-sS:UKPFrbF(Рc]ơ^p, D6SI}&*DN'*`NW䡕꼻NLymqjpF5f?s0bbpnr~iكӘ]+4̃{ ?Ix+O ^6Dl"1M"l]g~G0f/h3x.s2Sצk.iF 8-dgu| CrѼrQWY> ;ӭxt͠:Gf)QqEf%[M5p[r̜{M4L [|h]g F&S2fN#W~kkxn7[#Fh^A.3E8 8͹bA0XJ`H=CݎibNQNV꼻N-]054Ȉ%_"H &guڡ0!̷^^wױ.8"dd#=o ⊡Z35pH"8CZgOaHLaS,ҼP irH`vg- Ec 8|U`0NwOqIٌUbv+y8n=>qP#6^OtţWYf4/?N;h p®CA=V#VB L痲:p/Z\_;$#!\3 @XQP)Wm:#9/Z]-eP6?WHҀ`n nhep"&2z㷸Jn]51ۭ'b6p}OtF6z;7 ][ 󆺛ܜʏ`xweʸRBLs3(2S6Y !s(If!Flk8EdYf.i+ʼnݹQ p,#􋭦mCј[O[s~% 3]heNg󈣃UWN5ܳvmݡhޡ`.W?0 l>zݩ4{1QC-6NHD(?磹f3OTaNZ"/dS9N<2{y{sl䧆o>99 Y&Ӂ_cE9λqۚch^/s'Kdyyw;mt{ndi{sG^zVB13͛92(ur{N,&WoX${}5`,yP7R 0J@2tTHpwT爽9#85 2Y&C{Dvʛ=}`=G5Oa%=7{6jIG|- 차Yy$j5~%Eiu_qMHs. l@w=(3mی3{`UU%Q:<(_"qvSywn_/#I`c?n{H[3Wmt9'-;84{.UIzs4O +pa8g8/9OfUm,xފ\1wuK)?;}YV~Џ;mG64`,y̟ [d̟>gu94k]eЮrV?g~w :!ezv+7kr>tЂ'bA2`qƴɜwx ;c- ; ƶe`a}q9:#CE b9]+s]/Auصjfnn=6ƓmeHeZ#G=D~]'T뛞Ajuy8<&d<S%f.D M[mBUh"לner/HNJ?s4J F{kpFZ&@o|ĨpΧ[pL XC)OLLgDw)VpdBtJ(/t1p}gPw鶶^~+2% 6jj]k!q.۾2L#*3+Tg %SXUdh^b#UW+q6dfVn1ڣ90)4̌=4g&M)8[$3l~ xpΧJyXsz ,KxZ', &1FEZ2҅|Ϝ;<۞Zt[=k@؏F?Pn|h"Թ֞B(^.hlU5eQÁ?Xi_Ӏ67eRb͞\x{f׶jCC(oP&s{z~yFd+"{yAԾյg~ G>]|f;y>gOo(55&)†~ :.mki9WVҡMbΛS< 7r69`WZ ̡2'rHhV#*5il"/؏_,ALph<^u̵ć֮Qh7Y[Y&FHCZXS_ VeGO `谧EM#CvUk Qf׮c'Ύ S}s MthP#vǠuǵ4~-h8' 3q<\n9gSvde=_!^T7f]-0$Ě`rePpÅTǚ,dO33ȖГmbr"&} Y B4@D5NքoH3ry<]i5rjҳ`Dsh6tB ےh6`ЅjvHd Ѐ=rTHOЃvDDNgk꼻ض^y =v09y;w b4]h<9B2f+1-l:“09irf+LFPy; F6gYmC`FHg0YGE G!*'0?DtPKmn@O 1$#%IR&fL,M6tz[  vf}3f#0)z ;j`$\1K2MaҤ3`Hг n:T&&A59荃B�EiH\/څko"pڏTtۚWniޜ%ޕPeYx0Ѫ!9H{12E< ͦ8^NaƄjL~ل;\ݍn9ws}L\ fb SpPAYl)sG 8Fcw49lnMM0\4p&]⁎:?wHF+Z醽YBbE L0NHxZ `τ(֞kf?P?T#lfo7iB<ͩ(--Mop.Jat'eNOZ[9;;ryTX12ؽ#˰tFvTa()rLM MthP#=5mm?]ݍZpzV&bR3wsݨ/>1miPOYw|^4go,Sq>=A hK%\XjTnd?޻OB^ϡIl/N0LoCrfz Wui9X|3#ɢXzCLapP 3*R`삘]&t&:4λtۅKzvrۃ졉-鵉ɽArC %nrvs=K7>s#x]a]Ʀ'CY9b32gq.LO|z)  Wāy=\afs}Q!K|]Ӏs`!L*nd ,:kPɄ'{!;7y8ݦ sFE'N5ܳvht.LP/O񸙉U,]im5֠bi``Z?dcf4{ؔyKA^ `lGl=L'Em5}d1, i)oҠ;tbeOr_/nt`(6z vȆjHz>bxes`)ʳY/(0vS3e i&tYoywn[M =BPZ~>0 f'ܥG[bw79Cr=-쒹p.v+gtL+l$[ 2(б`9z}2&})I<@(3`^ZysJ'"ƭYL%mo¤߽Hz=}V61bD ֐mzD2NHlbN :u54pۦK|1БHڜ}\2C.Ýxf94{&6`\Y X q _nlivۢjOlJ)(p_Ʀ?]chSXBO œe]w/iF 80FaiyzLqV=b>Ӗ'Hs9t٤vP#6hmiWᶭ;>;S-{b=AOxYǴ6}⠞bwc^mKj`mk nӟKN\akj7ZLgQ\CZPQh)A.iYoQ8N΁JRF)||#0m1yYFRM<8qTힵsEI!=N=Coʧ`+Ͱw7GІm[]4)|r<G9bd:*dm>Ԁ:tVė2i;= f1$ Q5Ժz9ʼ`l>g"_+X :~{S`A ś,6pe.F"izzaWL2Kȼ&h`StRs%mo{`aYle`i SVz`e@41|wAu]M5>/9 Zj0ZʰVq9fOofXĸ{F|Œ=Y0R3ioS/VLy&2OB#Q̛zjMM"]k[u>x'O,fsh-*͗U#4`,d&]|j#Q^KkOpNꇚvݠ靧sb;{in[tIOAnj07OSSl}l14{i40HhDy'q3C}i4?i0G-˹״_駴0%r֠:.pn{Exn?|D3َ-ދUwWGf;y뵼'&$""=;R+Ϲ!y\hXd!s:h\"|S'ӝ)Rėyqe`nQArV-51kU7& M;83Z}מXPz}fVHXFI\f, ,a, F ;J b=fUV!a ݬ&u=4-0Tۚf+`!ZQEk"Yl'$g~jYb 346bq$+l/iFP/ 8a7 ,s&p39b3tTWne2Upp%r⠞boĄlA~j -$,`w4EaA_(lH텥'X܅>M;Y͛_=ʾ,R<0 nh`Ё9GfO֮2x S OI) hwl,ֵxlQ`wM-Է z6//" #v έyKjzK%g\ơ^,è A:Ďhp-]6sRxf+ b0T2%QiJR Oҕ׀YTXR॰[ʬiF<p[ㆃg Ty|-R|zVjn$&/inFMw x᩹q` 麫t:.v+z]B)Zw7ni+k8h`ObZ׋@vlU@qJղNk$*!^‡]nl+w;ow^VGvse 8Eǹ` `)AJ6rI_($vЀs`Y #:ۗg8]_}z s6FoO4 8w5%Y[NXWJ-ulU2VղvoN$ΣP3DSy½@\F*t)nK_/>pIB=_R 7-Ogf\ ġ^p,qcƁWNwnkywmkb 635{Nv'nr'^mi| D_Գ'g" e<ِO֘ UuH}&g{l}999d$~y*s'EDh6o7Ҁs`Y57,:xpt[<n[dJF"eУzm۾\b/2)ד׍nclss4xq]QAPءa?ksH[&(ɖStz.h64Xf͍q]9&|ywm6NXLhAp&jq nigi/TDsk EɅk,ݗ|X NkLL=ׅKQ]΁e ,;y pcjqT爽~n Ґuk,`"ؠbi˿q"fV9[QI{m uE1Xt^(냉 rw8b΅!zc{<(_}/.MF5~3%/T]O锾'~1nrG G'|*5$WVD A?jwS& eON&b8 ß>s"G"s%Nʵ. Kڼ8KwXzu^|:Gp+v/, HK3Ϯ=lA=͞xl,e써l>? "M1I~اpV\6PzC347q|8amV `;9BfݝQ@/phH (l~I6sY,eSOTh飰k;ŀ'`^n"K\!Q>H΁8tؕA=:m8i bfpvD=B7fOdS6Pⴵ9[- 2r$>-eu0T%5u笀44Ya9>ANHMėť#4XC]9ѝ3yӝm5im(s3,֥0Sl]=ك$Q(KvA'iM3s56^4Ko#<ݗ36Bh{^pn"/4Gh#yp#jTyt3rdM8#*I {JuoO-%.KIY&d"*I%E|ȮA!6Qp~(D6{Q:L ,`'pAuMAM n f%<2>A=#7.AeXQ "PF;^AOR8H%X5%R/okl$9[ETz-^96GQ55m%mPNꥁGXzd>t'+ywmwkkӽ=n'v7Y)H ycbCzL5)3|NU$E!ۜl s;5KU< -vCJi>6_TNTz2, _k659m< bG4 .&V^rPn<0]ބگ2 CA1`So[_[ބTj1RO8L+"й "*1UL=YEJ*T 8C*vE^rD|P间Zb74xt.&0f|hteܢuľ2w79sfO<c3,o Xn0$ZRv^sKX)9eje@q@a>\D UhQ69 0çIl9OqL^:@h=k`Ё3rl<`LxKGNf"Xܠbk6{`0%w7{b_l c+as56x@b§s(a46$dKm>ĭT=K5ME!4 Abx7uf虍BӚC&gu!910W$^1n0f-5]MbNsM7򾞱9H.9ԇKZ'Q g :!=^SxkG37P!v/76{>F0=Ц0ْ6`AfFE!g9Y* 16HiPJBҡ.POU6_IԇBStŶ1o:<2 },)ä8R,IwY f&̣PqS0 |Bִc#jzTm~4pjiNL庉bzTaشDJT\hhC4wI1CA:ĎhX,o +/}1Jp`ž3L\0`:u2rIp\f 0 lb04\_"mUrk0ys+3t_N$zp}0^h;d;]wx{d&Ħ}:J:ʶm(p#)9x" #2 x%nre69 xx^0 OKMȜte0_vMJ٠ob"n1/i EMb9([HEF Iq}wKQ g :!qAm<`L;x)XI2Td숆sPOf=]~ͪUn0g^^L# ,^]<,%p2G3zC;ozQΒd1LI7 M'd鳽T'N._ơ@hj`Ё60CDZ%6fǑ$MlYm=AWm9)oTږ1{a \^HOsAB .EJoe>Zn!d_^|9y~pk@ FEb/iU-h%m>. @h :!6=G=x`.Ɉe/Dx[g.l<=FH|k?y^(sT% 6hMF`8ճ 7W F`LԗQe*y iy do{dRfeI/iŞFeh 4n50@]l?0d&wu p&09/-*=f a09橗 HEwU69# `U;I Ak/3% C v vqv <=K LBGaC-|Sޡ#t C: Kڼ8 C :!v_Ƽ_;bi-JAhKAP# 1{ZbUCys \ ġ@hj`ЁN}n174JVf2rB;$Z`jy-1lK|Q3Q [ :!vc_b'0nP`[Slz>$ K+a06FqP@n,@4L(R;RLGR`7Tr'4 !Oλ.0Xb@ş"1XnН1 5ܻ(B40@]l]>`5=ZfWƤ)"q. Q+7˗^Ah 440@صdkyx`q9Sl8$fؿyF"t_dxd  V9cO4+ fI!_#}|.) gWeKzmpʚ;%K^ey550@]l'0!?]WM4{!Zm5o [1=J~E䬌TYg5H8y3%s̩AвIs 4bO*ag]s$-a}sMzOCʗ6gy yOǃ&4x?tC+})02a`o`Aҵy9;T]9"`A] őThaj 8Qyr֒C);fI 1 S/ѻj'z yOǃ&4x?t.~?,~nĞv7GP쵲fz٧7DM 3mv" \_2P(H2oZcHL$Z.Xf=qWb .ۚp2ƆB9N]oҨC;bצ}щf;:Mzٓv^=۴yfus$ΟWKudfeK "X*9 t9* EE k!oXρW긕&Ew/i'B]wx4~540@ kC xw nf ^s,/a0a__~ԌQ2J? ,4l5튞bWh/c'4 \Nt}9' &Hc }{Nz} d;_AF>Ainga 8@46(GasM31|]ǜ QJd7 52 ȳN J0k'Bb5.4 }8tI'VQmBĭ>҃PluĠD}t_vmW@;rmX,6#lQ bص'Maݡ4 7k^ҳZAguG&4 40@7صh '+iJ{T7`"k[DEB̆Y z`'xR`HLZ)`TI!64f! j-Y%, `ZM hW n YQ A:U/M+4p0X߂;0}wPg×`0 V <ᖡ݁0Y2@htCl{.ր+%_"P"`blxl(k}a|<4Fb Irs,:젴wy[ vp2Ll$ eoM  B#j`ЁnPGa0^jrSR/M6>w\Pb> \ C@913BL0(a46;\DGBGkNo CF On9I-SpIPM2@~IEj ^b+i :;;@hAA.^ K6 &$UW䞞`)9sG!- KIcHi`0R"ZP^C`ӳ9 &gr;%s NMYh 40{W!yhapF|-I Χ8@8@en *JK.U.2IecP*EJ֔ e~߯_YQW,i[Ƞ`X0MlĀDtv(4*C@Qt]Q0dƷ̄r NWo ޖqX͟&D_JK0 @QhPz ԺBy4ڂiHB9QYve]G@8pvGf @hztClceЀ+ xcV;a &j \T 0"fi5s~.;Pfe|LTYq~G4s 6WP.p (ܴJ U@h}j`\>W:4QN:fv; /)ubjla{uPQžEm.[@^!5'5}`z`(lEw ~B phP|MAgZw>4 50@@08Ewغ`  NZ~[{a΀@~Eh^;Dws4FZο)_03Db;gMEӋy͢2t uO~ mb~ceb]2"2dkGYjp/:ی琓/P08:;@h`AA^?jBwԀ,i1. %@/V񪅂Gg 9 a"h}f YwW*V9/u <IZIq$ť-sn/lMQ \Ftat^`ǔ߲A]`0|`VTnA":luh>C+LB9Т 5p>V 练.D94 tTlZ`nzمTO]`0WΆPg>T`1|%2gD/AH)iaszjh)usxZ'1ʉC540@]E׸ы> 8a0ph* <7^=5%Y`T# $գM@8 &"l :;@h`AA^?jBwԀzO]~`򌞍wTiҲcVx9=:9Hi1!(CA50@E`!vh`0tӔں`tzLsa{[0mG_N>%e?=Mˎ==; ZZ-mK'H:;t*4KNcdηa0 }-@ݎ@ˁ䇈?vYɂyU'_/қl)4/N 6j`Ё#V ' W3f]r. ﯱ/d^A rX^ -@˫AhZ$o!$tvX+*jB@Mt] Q0Ɠwa]]' ẁZp;Wbu̾~a##XJ':(Q~Q'y{;Z /} ;Bci`Ё{ c]64Q`dooq`O8_.:v;+V@%k\yU `?KBH\0ȃE-Eeh 4(j`ЁnP *CҀNaf z? ~ c@b~ OHKvfsi_9 D`=r!t:W* cݧ8+4HtmO ]54p/ 8a0(7ap+x# ﲽ=1c4ֶ`oSgm8 eĔnK5>@h 4Nh`-噻6vI͠w]`r-hTj뽵j8Y !(Pd[DJ2v [㡁tClgc+ ? 83"o̭105~ [ OaXy d,5wZϴ x ^֣^8 s#&4p0|b`J0ȋp`2WدNp.AFF np+7@/ ΃/D .AAž9EҀO;^}^y^ ;a0Jo@2C$U8 (6w5:-C!L6D{sr1v*4xL :!vDӢB4``@&.hA>^.j}N(~"iJ 7;!UN'9Z~i #30xίABtTq$$l@Ml2sܻ(;a0Z%v軶-p^vA9. f]3ٚf>4p 4ixYBSFD]m ;& Kk ^a;elNa4 \Ft}\C6 &)wm{cl$i^-!i ܢA:Ďh-= ^[T %(hw ɹA/vdAM/!0K :;ȅ 1B40@ >*;р _Xk`H"u?||Ie9wc%ywmDMh 4i`ЁnPkW!Cwр 簶|mRl4Tq}o/%Ǻ0k('BbAk\Ehh a0|!<}@ B l|yLz-+=bQ#p//k :;5?4J}0XPTa0KIԐa0_ǿl~!LFZ_ ]84hb~i"A8M̒+5?|cʧgC^~o4/~+=&ODAgZw>4 50@7kGMh` ^>  l"ro)@fZGf ġ@htC_wԀH S  lalU\x޴LK} \ͼۜRAguG&4 40@7صh ^XSڞk_>7e"pr)=JN(S[ЄBx#|a' U8_k {.S4]plE?\GAgGq(4 ,40@7 nhp`]DAt_>Rcy1P+)0>vlQ-Q \It+a.`)饰 _/ƢQ#VK#?0̃K@h 4`k`ЁnPkGC'k y'.TVK{|:6g,Ɔ +Rpr09l'bc6ly?]+= fgzK=Tԝ뛏ן}xpWzC{BoMtapx5kjVսS? ߺr#*ics& j3P/: (;wi*ow\y㪄 ¡G.%@ T 4х:I=2x֑گxGu'oq2 0@hO3 u2|v2 zHG.%@ T 4х:I=2x֑گx{}gս[[߹lf8v 0@hs ^heE Uףƽnzthu>|S 4хM{tPwun?K?~?h8nK]w|F@DWa[ 0]492xZ痏>xhGl!@q.4Fv]];6zmo} ևEXhB jG ;Eubp/W|WFlMtao PnFߝ=4FfMt桾tPwaʟu3僻=.4 |/ P_.z-W;,4{L#*lLB% %߫ݽ^"߯)דF$GF"@@h {K}}Wns_x"\0m! ֓xqCv?UV1\׈+=y@|O%@`2x|] W@k['unoۈ!]MtV7KP-#OKW{F]*\7;xNFlMtao Pnս'?L5L%@`5DWa> stream x= 0wu0 !IWB RoR{2hxy2ij-e|3ʷ6T8E-l(qѩ`fFe.e?9}3}P!s endstream endobj 103 0 obj 121 endobj 101 0 obj << /Type /Page /Parent 85 0 R /Resources 104 0 R /Contents 102 0 R /MediaBox [0 0 612 792] >> endobj 104 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 106 0 obj << /Length 107 0 R /Filter /FlateDecode >> stream xmOP+m _mmniZ#<*-PwQys?ra U|WDQ^ l?R)ш!.dP-4FT5hEU=bEQJ؋bd"n_&v(Z-yyAE%TM0z5Քb̼͛S*T5 ls1f]1~ P,Eh'Erʼn6  9ȍhȁ3#^f7&}})vtݐ )8th72p<&|9=y"Hkk0/ i%՚EFH@K*Z & c\NNT D߫=%2mV g,)uR7ҎIYG/ endstream endobj 107 0 obj 404 endobj 105 0 obj << /Type /Page /Parent 85 0 R /Resources 108 0 R /Contents 106 0 R /MediaBox [0 0 612 792] >> endobj 108 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 110 0 obj << /Length 111 0 R /Filter /FlateDecode >> stream x_o0)aRi`[K'oSJ(3mjU|9NȟNNr];<#rGc49~Blj[ ˮ IGXF(' gG1AT t+~F nש|]'\sZa'OX)ZrIr^jSwv.N2k2ۻzT&^d>y\tU# 7 ÙS2eqiM\ܮfnlN|blr&tE Ǖh^ѝ7tvC~>s u|J^SQUʣ [< S I0S@_(Ls'ނʸѶ2'l(W qB$mC_W9$̿~\A_XKkw|:8ܰMa&o d endstream endobj 111 0 obj 456 endobj 109 0 obj << /Type /Page /Parent 85 0 R /Resources 112 0 R /Contents 110 0 R /MediaBox [0 0 612 792] >> endobj 112 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 114 0 obj << /Length 115 0 R /Filter /FlateDecode >> stream xW]O0}ϯ RniR-II\Nt!{n:!(A.u ħl#bQ/v{ ]tm vRyS6G`3Ahܶ={WEwEIXS8UԌ!H1$4\}Hn w>DdI4w!r{"p,+hг4g&,oOS?`fpn#HK!LTEˆ>{7~]8 .򵧢; [\>RT%Qr,?t+"i5Y2M)ِ&]u t $W0@ !_!v1pji 1f!av63?ΏRϐ3@,, Y{K_B:B|>!piƖ fY;؄ ݕ*?ǾI`C oz6Kzy_Zvl|~}dGIΉqTXq$$r q*8 IٛN`+>vxg8 if4'8՚ip f*Qpj9Z'iqbT Eܪꕐf5ŶK4[bHniDGj̘8*CmhсۘdF@,]J1A$TӾM.ιP2OLuLP}B#)mhBƃpwdb֔AI#t=̥fsROEAk(YN'9> endobj 116 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 118 0 obj << /Length 119 0 R /Filter /FlateDecode >> stream xU]o0}ϯ8/E4N(Lt*mJR) M Mq2H֬y@!s}aD11(~|ard t_HV Ss}|@H2# oqG8/Lnp!74+5ĖmzJRɻSZya--Iꇘ;+rٽz#6w|~QI]2ʡ8@iy쉶: sapBKHik7i8%UhIzԕ.lnnR:0-~K:Ŵ;"ĵ .@U: {9cB "Y9䘅rwmCs+YZT*6d%=E:6N" ycu1<;uց>J92@v=r;_kv1/q!ϰ54& )NA6ȌA;/ % W=E!Drɳ  i.uHgI~?¼ ( endstream endobj 119 0 obj 564 endobj 117 0 obj << /Type /Page /Parent 85 0 R /Resources 120 0 R /Contents 118 0 R /MediaBox [0 0 612 792] >> endobj 120 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 123 0 obj << /Length 124 0 R /Filter /FlateDecode >> stream xV]o0}WJ(n]mDR)rihN,oU sν/ tyȀ %=d4bydgz|pY+}a!vN"\3!VP S3?%q"pgXVyθǫ!aghc_W uCF!unK2S ~RwZkߩ %vR.I3 D Jd `.BL3s:35ZkEc` .)۴'ӿ,]S,82݀NnP,P, KZ9hb .]քͥKVŠPLy̜Tl/ȊsLJGGSRŒoX:D:FBqV<Ó<c+xǷtq7ܘY !x*1n mHM azGf 湳U0ō(P_ӭWM+mن\5APz^Rk3>⃆ eCgNP;/vHHVBgtpb _D. endstream endobj 124 0 obj 611 endobj 121 0 obj << /Type /Page /Parent 122 0 R /Resources 125 0 R /Contents 123 0 R /MediaBox [0 0 612 792] >> endobj 125 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 127 0 obj << /Length 128 0 R /Filter /FlateDecode >> stream xXN@|WP%Ɨ\m# T}AgC]7w}Y{ 8`hvΞ3gf\3LM74Cfz6Fcztn]̮ t4~}E 0[C(["n0]Eèiھb_éK~;ۃla%^xOUG9&W#J"}|G]aJY!8:PIU 0j !7Hm5PK$oEuyH6gVu JHye0:ا[[94M;a-֒:;|Bi5 ͥA J`:[o@Q.=Qg>}Ȫ*0Q,;Ziꃴlj`B$=ypF塋#9kAxR );"0̀%¨C>_ 0qSI<-Oa<=T旧A')sJD4@v5W<߱̾$yAŽ@M<NJ*eZ"v1 _pxVR­frf *$g'I)<ض: F$N9W$Hv"tW)X=IX&g\ k_&owA^c9NƠ&2=v;tq: endstream endobj 128 0 obj 917 endobj 126 0 obj << /Type /Page /Parent 122 0 R /Resources 129 0 R /Contents 127 0 R /MediaBox [0 0 612 792] >> endobj 129 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 131 0 obj << /Length 132 0 R /Filter /FlateDecode >> stream xu 0E$E,u_)HQ*&OMS)nY g1 PsƱXq gÊAVEQn0/.RIu1`3>@hSb7Q tnI2ׯg&Z] !#"ṓy7d= `cIZ8)s}MHej;%`GD endstream endobj 132 0 obj 195 endobj 130 0 obj << /Type /Page /Parent 122 0 R /Resources 133 0 R /Contents 131 0 R /MediaBox [0 0 612 792] >> endobj 133 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 135 0 obj << /Length 136 0 R /Filter /FlateDecode >> stream x_o0Q@Zd2MbZoRedKfy -9fX";BL)rWH+'o9ի9lwѥx@h]ot!=uh%DH,}⒯""z`6!f4Q1;yxrl6Oi[^xoRA"ɧ$Ϲm0Y>/S+,|h87qpU`5C.a;UDvy r/coܟ^/[;4c ߿):z;Q'o$-'( endstream endobj 136 0 obj 332 endobj 134 0 obj << /Type /Page /Parent 122 0 R /Resources 137 0 R /Contents 135 0 R /MediaBox [0 0 612 792] >> endobj 137 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 139 0 obj << /Length 140 0 R /Filter /FlateDecode >> stream xUmk0x>0I+Ǖܗ`P\VGUF%Vj6kkBz_$,WlL>e[`s?`1~ TNfI :ÏqkbQ/X|q*Xo"<,k P|U 0fHx] vdz=$[.qkdMQ |ݮVMw BT@Zo2b:1Qn 5vojl  /-&V* /B+N]]d*y~KߊڤbҚNy3=+>2&-#p+_,p_-c_T$J4l[+\woAr)뤆ڀF+9^Wބuʟe =u+kvWt&+ߟnvZ6/ʧ2C8D:f!{O`* endstream endobj 140 0 obj 552 endobj 138 0 obj << /Type /Page /Parent 122 0 R /Resources 141 0 R /Contents 139 0 R /MediaBox [0 0 612 792] >> endobj 141 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 143 0 obj << /Length 144 0 R /Filter /FlateDecode >> stream xVo0_qW@LjZJv]ѦI"׸&0t B0{ݭV `4 @q &͊<3GX_a|zaaU $Fb2[ 9<;c կ7﫯`'K؊Ѳ-|Zʹp ?0d"ܳ~x/9>qLO ̾cRca\ý&pG{8%&NX  X^Ќwpw'zlBy,Xy YGD{h v0@:b9!bghrcItl]Hntx-&CgW22DE*$S;RAI#&$ꂗ%K^$&i♴H [w"9i/AVlĮꆃQ[2pf,OdVY|{*WV> endobj 145 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 147 0 obj << /Length 148 0 R /Filter /FlateDecode >> stream xWkO@_q&.&&Wmv ) U:3SYNLҡ{ιp khϹBo'(#@ G-Az+;,z-bkuѱ9_aūeecPǫWY0tH[N|. p #8XDG"N^eBp9S/sX>0, ۮȩ8^::##YD6ȿOa"̞1i `7=[D~%|=ZC@%Z ޼l~{!f H4Vx8\rAxU,I ats5iiWM&rd(f'"ddd"V>LI>km][C0%nKe6KW5/JcHITJt[PZ؅RpЫk@ѦlA{ҷ2Ss=aU:s]  :Oi^6[ACӹt8}toqEq|/dM3gr8` o#9 Y&goYRj5Y Їxxn_cC?C`+BIʗ [2;OD/{إ-%KPvhC,RmWc> endobj 149 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 151 0 obj << /Length 152 0 R /Filter /FlateDecode >> stream x=N 0+ݰjBsQhM~Iwü`gtA'0VcpED#-"PZJr+0"[\XGk-B Eع><_>q>~Z꾈$+ endstream endobj 152 0 obj 129 endobj 150 0 obj << /Type /Page /Parent 122 0 R /Resources 153 0 R /Contents 151 0 R /MediaBox [0 0 612 792] >> endobj 153 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 156 0 obj << /Length 157 0 R /Filter /FlateDecode >> stream x 0>;&񿢈B%VmM To9ɽ_+\Aӡ >C.A)B]^AIY֬T$ SL8({-#eh)yQ1 Hwq>%F$"aNh?b*JR7ѱJDI+ĹJ?. QN1*Fc̢"m,l[wλz` endstream endobj 157 0 obj 212 endobj 154 0 obj << /Type /Page /Parent 155 0 R /Resources 158 0 R /Contents 156 0 R /MediaBox [0 0 612 792] >> endobj 158 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 160 0 obj << /Length 161 0 R /Filter /FlateDecode >> stream x_O0' き2q1.}Ke hHm#8R6*GA(ڔ6fjGwhZX&Ǭv5kv "14!iC& tO -xmUSg]s jq @ZqŋOD?4 q\J+<2Rj}θ^s^rJ81/Li^1ȕ1, oR"p{}Oa]~iͷRg)ʮۃzcٿ endstream endobj 161 0 obj 313 endobj 159 0 obj << /Type /Page /Parent 155 0 R /Resources 162 0 R /Contents 160 0 R /MediaBox [0 0 612 792] >> endobj 162 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 164 0 obj << /Length 165 0 R /Filter /FlateDecode >> stream x_O0)N 0XHDxdS&[ kOo-$lsa<3GD0e<(Vώ2X;,CyvuIf>;&0FkJ?MM"VzQMTQ m+8<TH!D ωt 2;~hϴ\DFh\EkѾ\ ZrNb _*!c(q.\iit@j?Xs M 6?ޫk me bRn~nA3s:Fsb/6 /Jz; 㯌  '> endobj 166 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 168 0 obj << /Length 169 0 R /Filter /FlateDecode >> stream xMk@/hjTJ=yE(k2jJb,,n-H 3$%'- N?EZmx1]0;ʿ&slؽ.^[G.ќ C( Z|K /nY_[ M<( &AX">Rl\S&MxڤyJpVъԼ9o=\:ʲc [HRKHZgy4򯀠?P_"i(p{*l:~ twc;Bcld/^fb6iчG㔂Eۭf*,I#ؓM֡\@C~vgN xi_bN. endstream endobj 169 0 obj 398 endobj 167 0 obj << /Type /Page /Parent 155 0 R /Resources 170 0 R /Contents 168 0 R /MediaBox [0 0 612 792] >> endobj 170 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 172 0 obj << /Length 173 0 R /Filter /FlateDecode >> stream xKO@'Xv HFW.$6!1c{`g_tڔj$@h9| p{< 3rHP%2,w95O5ٌFG׳ W暯|p ѳ8@47($D\W.iҷwUd9UءU*$|!(DC}w;2jUh*K> endobj 174 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 176 0 obj << /Length 177 0 R /Filter /FlateDecode >> stream xKo@G0\U]EU13(U{D5ϙ#nKoFNGdedAŮʧlFSe~]f_96^ϓbpG'q=@ 5!~WݬQ)mF&NhJ( \C+xʤk6x|3Rrr7uKpJ!EJj]PIN> endobj 178 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 180 0 obj << /Length 181 0 R /Filter /FlateDecode >> stream xn@G"pa *ji -xݻk`B@g33<Ohוe׷r$,x"Ԇ]-a6\X~X;4-džqympghܲAdQ$sH L)4h#>yd[o&h\[| qR)6q$M˺~ee'%v03sƸ|͎ߦ.XMII.4u " t{BװI]_{ۚOM_V3 0CGٍuKԋH2 E$)䀘 {lVm^5A!kMVvo0ø`0≤gi=A69MT@ӶiB5|k.0e^&LUE)U7NU|ErtፔqG`j+9&nW%]K Rk|<*E Ţ"J)juts'|H .&ʿEVdk9dQ7I3d &K 7X[p?opN* endstream endobj 181 0 obj 672 endobj 179 0 obj << /Type /Page /Parent 155 0 R /Resources 182 0 R /Contents 180 0 R /MediaBox [0 0 612 792] >> endobj 182 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 184 0 obj << /Length 185 0 R /Filter /FlateDecode >> stream x[o@+D$Uݪy-ۼ ewiBq"QFym|f93\}9l62RN_gXܟ'i96^ӋaO[ $0]Ar5!`-I+"۫ݟ8h-|,%23.R:S [%}rۍ$z; i Uy/УG7`w˩/W#2݀bTх=4GLk4>7^L~0r^__.1>Ly*Nϓ$'ǬhV&~P̏3S5]Z9#RXM=JٜXwC~۩OT)LM~%'rd0Mb ioMj_>O̔ڌ1%!*)jrJsb[e=NYxhy@:Q BeE:,0;.{ktY?2pc/ .;Ɖ]O؃ڄ@ 8i endstream endobj 185 0 obj 682 endobj 183 0 obj << /Type /Page /Parent 155 0 R /Resources 186 0 R /Contents 184 0 R /MediaBox [0 0 612 792] >> endobj 186 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 189 0 obj << /Length 190 0 R /Filter /FlateDecode >> stream x݋@W>5ѬR*Cٖnv_2&WM̸kKN>q݀Ü9{G}l&68n҂'[}H/}g#Xx²ӗ_rl7=} ;'P, P[2IӮ{7_Y<5QyT4r%X%">QR:exM k9[Zt5QnXA RĤ6)u%"  G5na/x[9F/LjF+ 'DP=nb}w<{⿾ zsM~zĕ\t4ʷ[~Y n;Nx@pPr= yHxq ¾9)"Ey_]\غ9bZx2Z( kM^vo¢t:\oe<A69]mmӄ>Z,T[bɼL HRj7AU$ix'vuor2MB1 ln]K>*ъ z3ZFˆ:ICSl{vɖ_ÛVID}8/,Ct6. endstream endobj 190 0 obj 627 endobj 187 0 obj << /Type /Page /Parent 188 0 R /Resources 191 0 R /Contents 189 0 R /MediaBox [0 0 612 792] >> endobj 191 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 193 0 obj << /Length 194 0 R /Filter /FlateDecode >> stream xe 0}9i6?9BĈڤMUp.Lh1/U@'-ouPT/\dAv VEȕX#{g8ōoz܆;tʵ2B/?O厩 9䉗XG~j;T endstream endobj 194 0 obj 165 endobj 192 0 obj << /Type /Page /Parent 188 0 R /Resources 195 0 R /Contents 193 0 R /MediaBox [0 0 612 792] >> endobj 195 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 197 0 obj << /Length 198 0 R /Filter /FlateDecode >> stream xTMO0+.FɶD=%Df!F(p%PfpK(;,~\! )`0 Ld!Vwв0[],fF6خ~ O_Ō`v薘 Ax&lEf:0ha&$6`ȚDp7:#e{>\|g$g 9kڂ@sdOҌt, z,:l8Bqp3WÖ*@Ke6 qN֒AY^MjFF}lj )4^BEya!WݫG(-lU'ۜ,凈MtŅ. )S e2<A Gi_2z*Sğ̀#AH)`%~t_ endstream endobj 198 0 obj 429 endobj 196 0 obj << /Type /Page /Parent 188 0 R /Resources 199 0 R /Contents 197 0 R /MediaBox [0 0 612 792] >> endobj 199 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 201 0 obj << /Length 202 0 R /Filter /FlateDecode >> stream xV]o0}W\iHbHۢm]JmK\`@}a`:MϽ_[8[py0zR O2$SS.dD~5@m,zI'sA#bW;G_1[aLN5΋+4|V`F dgivq& bu~01xdZ@JM3ap(SaBkTPq AGEq [J+NpsfР؏NM눌Dbo?0Fu)M56hoB-sa\X b{V0\h<**^4Dtj>roR~QQTea8QvA}n@H3?1 .w5ҠPb :H~J t9h}$>e,{ࢷAZ^%p7N~2ʖ5Pb_ $8GxHfvM*G;)Dޑ *֐j]%\kX;^7nǒ`ѓk)ə1K.ԅ@0ko2\K 7l޳+Mq]_vͥ$ 0 q,f+Lu*v1^oi<hlv|C S(f endstream endobj 202 0 obj 722 endobj 200 0 obj << /Type /Page /Parent 188 0 R /Resources 203 0 R /Contents 201 0 R /MediaBox [0 0 612 792] >> endobj 203 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 205 0 obj << /Length 206 0 R /Filter /FlateDecode >> stream x1 07` 1iVE7BsQ4I Apܽ{C$i0VbQG'K`KdRPH;H]o9ilռTpw⥍7oc@^=pp?~6V7\ endstream endobj 206 0 obj 152 endobj 204 0 obj << /Type /Page /Parent 188 0 R /Resources 207 0 R /Contents 205 0 R /MediaBox [0 0 612 792] >> endobj 207 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 209 0 obj << /Length 210 0 R /Filter /FlateDecode >> stream x_K@)laYK fq&Ia%wLZ)C=7ϸ3~-GI kő*JͷN8.v!T[M3f=>Bى"\ *AvSpnζUwX썌kEe@܉$H2Mq9Oax8 CFkYLIA y endstream endobj 210 0 obj 470 endobj 208 0 obj << /Type /Page /Parent 188 0 R /Resources 211 0 R /Contents 209 0 R /MediaBox [0 0 612 792] >> endobj 211 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 213 0 obj << /Length 214 0 R /Filter /FlateDecode >> stream xVn0}W\iJb7 Ij6a: $lZHC (Up*nsϹ, B 2z`BR,OEa o] Dnf&RS OƢ n d!3܍ܒMڪ~:s}öK°=YdQtEtV=C;tnRIZwҚ.fd "59EOiXH Rt(O~Bi !ʑAs8\4\F}tjPD2,oL~;u'`&ݹ|ںu UbgHήw.Dd#% /{_eY/'Z0)q&b9K~~Q}i8_1t?Z>V;%޽Z77\.\WmEa儬BBwxJAm= endstream endobj 214 0 obj 671 endobj 212 0 obj << /Type /Page /Parent 188 0 R /Resources 215 0 R /Contents 213 0 R /MediaBox [0 0 612 792] >> endobj 215 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 217 0 obj << /Length 218 0 R /Filter /FlateDecode >> stream xXko6_qXa4hbފY-R'?KiJY:K76-,w„@  se4Z +0-P sdذgj*[p_0\\\wVU`iL^>e!7mL㽰,t k9^ ;IĨG>{zEJ2lLQЍ Vb'`M@ELqAAᦐnBo!b#,(.-R٫g 2/z0hMɻtNPBKƢ|'gx:RtsT:Ȃ]/Utq0E*肽9>…h@EYrޞŠ0"4"ų35QmY5]9Tx#vxJT9;Hvgꪼ CM=i\Bo(K k~@l9:[ʢcźKFg9]KHrJ/kl D-N"z՚weXdG|8]FYqf//T\"M:o' Xq+lg |%pv Hp 9س_ilf[κƢDSScz0/SDur'] Vp )9VGI̭Ƃn׺ {ޱ qGߢl<]t^7Xʑ.g=xr5iI W99 G ;AʡcQ!~ȱj> endobj 219 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 222 0 obj << /Length 223 0 R /Filter /FlateDecode >> stream x== 0w3Ccvn YR;hXpׂgAl::dQir,+e\A Gmڣ.*U"bj-!Q߭ͩSau:>_"r endstream endobj 223 0 obj 122 endobj 220 0 obj << /Type /Page /Parent 221 0 R /Resources 224 0 R /Contents 222 0 R /MediaBox [0 0 612 792] >> endobj 224 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 226 0 obj << /Length 227 0 R /Filter /FlateDecode >> stream x_o0)$@j 6?@Ӫu6M1$4vMSdf'YvJxb {9pgH(FX;Bn$W$7'iP8eNz51QJ}б ؅`6!-+kk|7]`/y\7O|*jп<8m&Q%"\QI|J|Ǥ|Ii<1/_.Rkĸtrz'dΒeO΅P:aJ#_7k񔅹Αra"B,:6)DO"}( .J D;JEԬFE&4#۳FlE&:om6#CiƷ0r൏::q5%c_;G@UZ= Y =/|nbT+zWNSB%qHRUHÒ4{7 endstream endobj 227 0 obj 567 endobj 225 0 obj << /Type /Page /Parent 221 0 R /Resources 228 0 R /Contents 226 0 R /MediaBox [0 0 612 792] >> endobj 228 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 230 0 obj << /Length 231 0 R /Filter /FlateDecode >> stream xW]o0}W܇Jmb hZNM"E8-ku?^c Nټ!I8s+ą؅wHY,:%NVçW)>m2p=p?S,Xn(<I٤y&± Pf a2 Y}6f۸gU`YlBFUZLkw o4YEdRBO&%d]=m׫˂Yrޠ!LeL U 7couҞr0_[KQr Z ff$@8WH|E߂Օ$~읝Ysh6hafo1-X1xH7;6PyBT<`Ia,_EÜSzyu1-o8bt `z(mrDVD(tqC<[t![8?vqbpphނ_=#luvY# 0~=MD\OiI׌> endobj 232 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 234 0 obj << /Length 235 0 R /Filter /FlateDecode >> stream xo0߲83[ .&uvN2WIlм}̰A` '!$&` Th(a%3p/ԫG!hm ,\xBuO> endobj 236 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 238 0 obj << /Length 239 0 R /Filter /FlateDecode >> stream xVK0W|U/p0yJHz.^VB1_2(%!3| `6@qG%45SG3 AğLư9YXmaKjC1! jK_ _*̲3S0.QG.!t'T}Z!=WM$NӪl栍<\Of  NTI!t3m@>U*2]KYj$nw<{h5u:|;LFoq=yJ DAtYI9;4A3{&7nF&Z }83HJ ` I7j(BRT{w 44W9 e\Rm럡uX" X (/Ŕ0nV)`}xa,2>mfeUw)'4#Eo`U}! `|CjB?yD&MbC0V|)d>W,WPGz(&6"/f߮!eՊpKK endstream endobj 239 0 obj 643 endobj 237 0 obj << /Type /Page /Parent 221 0 R /Resources 240 0 R /Contents 238 0 R /MediaBox [0 0 612 792] >> endobj 240 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 242 0 obj << /Length 243 0 R /Filter /FlateDecode >> stream xX]o@|Wl<$)A .$]ƲzWx 7||( G.Vsˇ|z5tN<sg |0M鼋t w ipQْ3Sy^INùcHk-e30w<Ө^γW~U0Qh!uvA.W^yYuekS,HU,GO~:r[/A0Gy|s=ٱS6uatld3hVڃK,ȡ:2 sK=xټ7؆:FVrB Lh+Nҵ54ШF`E o?Vd[6x K\7~?[% endstream endobj 243 0 obj 846 endobj 241 0 obj << /Type /Page /Parent 221 0 R /Resources 244 0 R /Contents 242 0 R /MediaBox [0 0 612 792] >> endobj 244 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 246 0 obj << /Length 247 0 R /Filter /FlateDecode >> stream xXQo0~ϯi}hIhP NMk6ML0%#qhX!Jwݗ_^W`0 t܅AWeit<+f9Żx=roa;a$ [Q(LtE:fvVb)`O%'3Z%"`Gtm׽}x]itʚi8.0*}9P셛JX00lgIFS1}A&-9_(oIJb>G{(ϟ/e#^<%U݀:>ٶC {c'Bx+@$BX%5P씊,eǥ02.hc $8M]$5 "izvo52x‚$X(ܠRP8!_k͚C*HA9\aA*ĎFM %ΟK5Q.ϩFg1e;TYuVPȎouݪ_lϭ9G, 6B EOU*HԞlϫDU׌ChY**K^,ZCJp%y9rJߦf֥EXc(G 'Y햪XNZ$ mJҿl-| ~rs{zTo2W2Zvz2#[fdn\b  ilB=3P92=c6 -B*vH 8ֲe:T!&-ӡ&#P@c6|#P J2X]>mF{ pP֟zVgemy|3S ^RSӉ1E?< endstream endobj 247 0 obj 874 endobj 245 0 obj << /Type /Page /Parent 221 0 R /Resources 248 0 R /Contents 246 0 R /MediaBox [0 0 612 792] >> endobj 248 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 250 0 obj << /Length 251 0 R /Filter /FlateDecode >> stream xn0w?@j vHCRݩ]qi*lǑxB.`}sXb!;&uEΫ[c֣9lwAeK<ŠJ֫C}6KL^'\~ L![;Xo7ZB5<\ui%m-TCWw7#=?TvE -e(zCΜXnF]^x_ԣg.'گQ߀L}a 4݉y@rߩ+NWoU[+^_xb endstream endobj 251 0 obj 286 endobj 249 0 obj << /Type /Page /Parent 221 0 R /Resources 252 0 R /Contents 250 0 R /MediaBox [0 0 612 792] >> endobj 252 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 255 0 obj << /Length 256 0 R /Filter /FlateDecode >> stream xuAO 0[zuă1KxمTn-t:vq8Glq +cyk`q FcCʓC &0;OsRdX{N@b$Ck}ƺ\(x,cPhN ɜ+N Dna{B6xv5r 5SwS5 Ice1³u|mM@&gtQ,@3*4//ګw\q lv endstream endobj 256 0 obj 259 endobj 253 0 obj << /Type /Page /Parent 254 0 R /Resources 257 0 R /Contents 255 0 R /MediaBox [0 0 612 792] >> endobj 257 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 259 0 obj << /Length 260 0 R /Filter /FlateDecode >> stream xQN0o@N(F@Pd`T׍8пNDZ) ӝ޻|PwƑfG7TA*EmOW Ry01r),OBn1ZskEBdFbٯɄ_kS9zpZ 'HyFbI4>r]pw%C:Nlp)}UcC9o?``h<<>ͧj֥nr,cK,> endobj 261 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 262 0 obj << /Type /ExtGState /AAPL:AA true >> endobj 263 0 obj << /Type /ExtGState /AAPL:AA false >> endobj 265 0 obj << /Length 266 0 R /Filter /FlateDecode >> stream xVQ@~WL&DEOFz m_L]V,¿삊Z}ag'';# r?VB?APe[/ڎA!j3[_Mrsc»o`p `B5>U-Uo?cicǝǟ2, L [!ؓc`-HGEYh7Klܽjh6 GFe:/t3)uQAe0};2cjz@x}^s@nűBbIA-c,Ш"`Q4qHl@ݸBm6~IȋMHe\2]bœ⠼ ]{.k0b)檎R:NT7Κ^M8{E9G?͗eXXΞb&sw!ߕ ~|b屄M& K.D*K !c9ݍª̢=  RW- v {ZPQ_uhC'oq(YP1 Ǻ\-UR_3; L2otC;ђ,D q-l_O endstream endobj 266 0 obj 637 endobj 264 0 obj << /Type /Page /Parent 254 0 R /Resources 267 0 R /Contents 265 0 R /MediaBox [0 0 612 792] >> endobj 267 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 269 0 obj << /Length 270 0 R /Filter /FlateDecode >> stream xVn0}+IIb$SuնǮSDB8+0)8 liڇ^sn ޔBH8y@]C☿a>́}c>mrF90hOaUIJg0 Z00dZeh匿Ky%4:D8yEHť^@? 7iM@fIycg0zoҹq> endobj 271 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 273 0 obj << /Length 274 0 R /Filter /FlateDecode >> stream xWn6}W ׊ǽ۾XN)G7?Rڒ['QMΜ3#x; 7JRLhyج"8~Yato=XmtOaч%/]װL͑fu;]ۓo~\KD|B80s3e3,t=<$BwTO&6p:<iW>Q+wbI_X;.ZB>ln Ws yX|6^_v.M/ԡJAc4I.*V= 뉍Ö%j_.U%b *ydNQ('@fY\V`1g"hfV=#w.ٚP5ZMP-Gdjy>roEHt3(P  >y /Mo vRv^ZP9gi6Έni C ˄8ԛڽB~}> ExaCآ xl+h QwbL$ t%ٟƜXw.p[KV퐳YK<`!u q>cRb״zKW/- zh55U]5='nuaC0jepS%U>Qp20fNFcO 4E@R71LvCUWэ*׌f noG"ec( BrQACMp6X 2F}+6@DIHyшqUS",Ӌei|Gz ?1Sqc vlA+`p^ڪҭeǜAٖ,L/zgRk 疒IG 8TP'H|tX3SU2.{IDmQvi\ endstream endobj 274 0 obj 990 endobj 272 0 obj << /Type /Page /Parent 254 0 R /Resources 275 0 R /Contents 273 0 R /MediaBox [0 0 612 792] >> endobj 275 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /ExtGState << /Gs2 262 0 R /Gs1 263 0 R >> /Font << /F4.0 44 0 R >> >> endobj 277 0 obj << /Length 278 0 R /Filter /FlateDecode >> stream x]O0+΅ (D251$>*lz1N3],{qv Q $| U%j#15?a$k1w~Es*dB|a`pkcNE*Nq 2d x[[)MzMH,źn0! s h]t:( l/7IR=1|iYu5uH/|:5+lY!N!}4QsɖL+ 'x(N^J7J]- 6zydut/?-} Q endstream endobj 278 0 obj 339 endobj 276 0 obj << /Type /Page /Parent 254 0 R /Resources 279 0 R /Contents 277 0 R /MediaBox [0 0 612 792] >> endobj 279 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 281 0 obj << /Length 282 0 R /Filter /FlateDecode >> stream xQo0RaajڤMD6qS*0vد1BLr NolHD"M=X}ִU*kҌ0ZItr!\J9ܧABF"ψmF/cC5KkyF鄔wR=,.ke DtiDGgZ-uޣZ߰ٸ6ץ{" U*؟T-a;핷bk5'u:&oWߖJ# }PݦiЦ,CD= %Dd,(A|"Mpu D S_ȳ ݡK@F2!  fy%Fj.ta:SXj8‹iڋ&L*rv(>M7r8(x֞dBeZ͐ Wʕ|= ;7 ' endstream endobj 282 0 obj 531 endobj 280 0 obj << /Type /Page /Parent 254 0 R /Resources 283 0 R /Contents 281 0 R /MediaBox [0 0 612 792] >> endobj 283 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 285 0 obj << /Length 286 0 R /Filter /FlateDecode >> stream xKo@0 *FҨm҅ ֍ aT \; B"p~HP|hO#8b%"%#l@`bB- jOÃGWW؁>p؅'r #^_+4kE؂1|v-|p|"{-Hp7QKX]"!ں$vBd5FBy Q%Q8D>Zj̊Up 1Ѵl2_i02e6b Jt'nȔ#޷!<{c?Vr.INS]ބ/ onaQ]GʀCd&<;']F6bKb:vzx\܋'ݱHzꂴG7$!SnN("SZ_PP& _z2q1 $niorLr~KL;< endstream endobj 286 0 obj 462 endobj 284 0 obj << /Type /Page /Parent 254 0 R /Resources 287 0 R /Contents 285 0 R /MediaBox [0 0 612 792] >> endobj 287 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 290 0 obj << /Length 291 0 R /Filter /FlateDecode >> stream xT]o0}WH@ҭ QIƴUT6(R`x#!9_HpE0Br Bi$tcpoӫ-wйY?2 aٶ݅wo\zYW.Y'{[Pp$eR,laY>bM-<ʼ(xKy\A4 =H)vʅPMY endstream endobj 291 0 obj 477 endobj 288 0 obj << /Type /Page /Parent 289 0 R /Resources 292 0 R /Contents 290 0 R /MediaBox [0 0 612 792] >> endobj 292 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 294 0 obj << /Length 295 0 R /Filter /FlateDecode >> stream xOs0|=\Ҧga "Kdc;d$O}+W0˳l6( se-#ta|v/L ,\p<[m.5L`O6>uZ7<(O  P̯k}-Ѱ0biLAe%XfD(/ )$@={.KacŦW;xNt* FfS! #\U%Hʤ ezEG2Ǫ?˱m9d!I٤2+~L&Rѵ ,"ubiflRXea!2rj+(9<2i[3WLS&,kս+;]? 3E@_8C5a+x1.D~t !5aZtζ# j:GBM/*jӯtvF"9͚u:]F~H(1qQ OSoJN lGHǑyRId8G$o%BKqPK7ҐOё q 8 ) t4 e04g3D|H5Nu6O*MN+ˆN᠑iL9DEGcd*+8 mUby:5!8 __T૔Z.ٮQ~ R-)euo-&$-uQ?5WU$Tt#E+_Qm +ٖpROZJ _7i£RC F䨠i3ܼ ւb FJltr*n⼞SOX(|'ê<|P](~P"Ugf=g8{߁k"9-!5޸DMɩTӘRrrhkƔ̨ endstream endobj 295 0 obj 1006 endobj 293 0 obj << /Type /Page /Parent 289 0 R /Resources 296 0 R /Contents 294 0 R /MediaBox [0 0 612 792] >> endobj 296 0 obj << /ProcSet [ /PDF /Text ] /ColorSpace << /Cs1 7 0 R >> /Font << /F4.0 44 0 R >> >> endobj 3 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 2 0 R 11 0 R 18 0 R 24 0 R 30 0 R 36 0 R 40 0 R 45 0 R ] >> endobj 50 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 49 0 R 54 0 R 58 0 R 62 0 R 68 0 R 72 0 R 76 0 R 80 0 R ] >> endobj 85 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 84 0 R 91 0 R 95 0 R 101 0 R 105 0 R 109 0 R 113 0 R 117 0 R ] >> endobj 122 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 121 0 R 126 0 R 130 0 R 134 0 R 138 0 R 142 0 R 146 0 R 150 0 R ] >> endobj 155 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 154 0 R 159 0 R 163 0 R 167 0 R 171 0 R 175 0 R 179 0 R 183 0 R ] >> endobj 188 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 187 0 R 192 0 R 196 0 R 200 0 R 204 0 R 208 0 R 212 0 R 216 0 R ] >> endobj 221 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 220 0 R 225 0 R 229 0 R 233 0 R 237 0 R 241 0 R 245 0 R 249 0 R ] >> endobj 254 0 obj << /Type /Pages /Parent 297 0 R /Count 8 /Kids [ 253 0 R 258 0 R 264 0 R 268 0 R 272 0 R 276 0 R 280 0 R 284 0 R ] >> endobj 289 0 obj << /Type /Pages /Parent 298 0 R /Count 2 /Kids [ 288 0 R 293 0 R ] >> endobj 297 0 obj << /Type /Pages /Parent 299 0 R /Count 64 /Kids [ 3 0 R 50 0 R 85 0 R 122 0 R 155 0 R 188 0 R 221 0 R 254 0 R ] >> endobj 298 0 obj << /Type /Pages /Parent 299 0 R /Count 2 /Kids [ 289 0 R ] >> endobj 299 0 obj << /Type /Pages /MediaBox [0 0 612 792] /Count 66 /Kids [ 297 0 R 298 0 R ] >> endobj 300 0 obj << /Type /Catalog /Pages 299 0 R >> endobj 301 0 obj << /Length 302 0 R /Length1 488 /Filter /FlateDecode >> stream x=KPK5T*jZZ-"bPQŦm"MJ ftDɿQ `w88xB7w;K Xyf*!ĵN^ v 8)JTXd-Vdwӝ#^ oȞR9 ,Jĺȓ!Cs yS.nMKa;u^['~`{ 36Ucg%fST endstream endobj 302 0 obj 377 endobj 303 0 obj << /Type /FontDescriptor /Ascent 917 /CapHeight 815 /Descent -313 /Flags 4 /FontBBox [36 0 438 800] /FontName /RDYKHS+OpenSymbol /ItalicAngle 0 /StemV 0 /MaxWidth 1168 /XHeight 611 /FontFile2 301 0 R >> endobj 304 0 obj [ 356 ] endobj 305 0 obj << /Length 306 0 R /Filter /FlateDecode >> stream x]j0D= gc():EAk࿯zAz3gHJ` X gux%mU&*a[@mWF$\h '} kwD:rċAO>S/[F8<*Dc!'it4&U$j~O=?c endstream endobj 306 0 obj 207 endobj 17 0 obj << /Type /Font /Subtype /TrueType /BaseFont /RDYKHS+OpenSymbol /FontDescriptor 303 0 R /Widths 304 0 R /FirstChar 33 /LastChar 33 /ToUnicode 305 0 R >> endobj 307 0 obj << /Length 308 0 R /Length1 15820 /Filter /FlateDecode >> stream x{y|[ŵ\.ٖdٺ,˒mٲ؎c_Y'I1Y$,qlaIZ(ҖPxmd)@q@_JH}lsey9g3gfΜ93PB+/&/P#R^b|B]Bk/0m я;׼^Oe6!w֮ZIHy5O[J? ^5ʗ߂x+մ!>/|X+o%yiB"dž7oN>e凷ްi2o$ъ4DC~ #KRd y@b__jCxuӓ)<)8YF^Kb2l"W})rHT #+V>Lo<; m}Uue? 2yGEkaRg!oQڡ WCBmMnBߞWC9 z3$)z9c~s]Ď%ҽA3, u%-*N r= 4 S 93z^$ "g?iWv:>' 2d!hB.]v GTal}Ys אGЯ_.C]-g@{cd-z r|DTO4h-]AY XB8%3{+Vj\O%בqrd %[h/?gDž e*YuQ363Lֹj҃g! g@= I3\HZN+~m!=B]z-v;;ƅ!"t /ߩ>ML>=y6kFf̞QF@F{a2 &0R ɍDhzwѽv }1Œ,'¦l>Ih.#*|کyT洶Tg&NLuwN5<\bW-x2h4]X!" XuyVw1 2ISMuxrmȴZjm^Cow9@O~_I6}"YQ/ YV=fvϯ7'U P&t  7 {pXUHժڠz^ z>[=G\R}>gԿPVg5kd4hӴڛ?Ӿ`O=h}:BUYAʶ dwGA{тUd9O]_oBT Lb/'KTw|a\t0Cu%:;IerT瓳q^贋\XA7wd5֭"O7QGvq>9e{U6֢qFQ0<+: l|:U*y+1 b}D Tu xqC]~Z1Ne^Wcwa먙\*运ikߑ{>P@J^Pȷ)aj S1M@%diAHXOI=]A&%heٻ(G X\jp<|̦7*2}EKitFC_55r?kJ=zfOO+Z1{F/ }hu=ٻ_?E*Ⱥ}p4.JXt MhJ SKҡh4pѶcLf%^WY#>BC1|sF&+I,}d'EX?y9Kx9_`ɇIARb-ttmL!{u.{Խpisv/Z, !o*t 4bAɅQ.[ "}ƤŨWe:XB}]IV(=K)bSH6Fkvr_kqнK^t޼ua1ۻKu;Ɏ|ViQ3e{; F4}fO~te-m26*EKqq_Q>MWQ&I(?eqpl;mFV=+0Y\"reI֢ 䒺f  hDM4VU]sYF%/_"uLg̰.T h =c0OȗnfI$RN> @o`< h)H*OAvJhU^FAjYo$kEN83 RPnQkfe:ębJL R 3M0(nT[4\0_e+T}eEP"?eE@-wbcmF)={zs) Q(hQp k uq - Iux!7f: ~*jyn"$1c`bEP7v^ O\2p߼Ʀ@iTI桎ǯsE8Y_( l={'m8pSaUTsY+ D+[ͷI#ZUlwnQ 7jl !ƿ4}~h>bf:D|U%$JtR4ꭶoHYWUWս:V2msyyoFܑ1M=|<{lɭ/| p+P\j(HY$Yc4חc@@ -[)/`fʸL f]̿PH-m5{RZ0=MzB%7̳Z>|-9x"|RGv_7X-Ҫ*ߢN~! >Jk?X0ާr6ǵ#W]V8qBFė{k7%nk{kkGjM꟡ol\Mͽ=|ZAPAWz 692BynHgUlN{,h)E&R]R#UĐ~#22+u"NWY//j`C˙r9.7nV.)ܕwi`sOߨj|J!O:E5>+rք# l͎Vw6.N]ЊF=E"k~?awjjdw6P{z|orEu2KRB(Q"ֱhIwo6aOoJ” D?0)[h<,_(T;q;qD.; QV 4L^P6tc͊мW[[7;Z)K>OXٝEuiJKYYI芉M߱ ;&fKۊc -dO ³c:Ocߜ`kg4!5h{"azvЀ(H/6.v7gz@e Nr݀WHhWa{B:CiA_fk[ ew Q ~Q i9ՑX%htKL4  6k4MؾOS[Ek󖔺n'E/)/R D ;n~Xۧ;skJ]ŽM]a`"_9Դ>0/s|RiϠ=qQ{\ ogKxSMf%ya`i(Ac叩SG?˫i43|պRWi\g讋?TPAl]V"4j妘k*l r,,{EWݕh* ?|~%1&ȱe#R󝘶3Tb͋-yho6=՘Ɏ3S#mj*jiLKj6,-6jiXl'!J-V[WFPD5D$KP'"B'*d4a\B`/Q-MgDg㉁-5YOQ (4.E쌽!MODDQ\QҌnB}hFDL7@ApS68p (nӁq%A:=8ksVEŪdU sM4!JPT($)JXbV qѰse4V6V6361PQ ğ(sWߣYܲ`p~l>Ğ5Spq dkhP Q[2ss xfp-Wd[apwm?r NmݴSn@+ieuٞξ3'CqUX3&C3ܶf_޷}za; 5 ‘ ߬A q^+&oʅbëbvk)(x@rc4-1-qHC52bݖ:)F*[qrbMq9ewru4qI : cՉsR: ew{Лb>ƼcXz0tc8miA?E_;1:ih5ԺBY^Im7MQI 'gLxVNW,\Bos?=Dtz9dD i:C0cTɡh¢"N!rE!醙 *Ę4.1"ҠăjYGC]3/; L yłb%^Z{K[RFmFs f/u9X,&>u‹D#0m9֝Ffqu=ggqvkMÖݮ}ӈeKǏ~[㈯'L1Ap, ]^z믝~x0[f ^篿}Fg 3S#K `XcfO_c yڢ%*M~asυ~k}OZN` Haɠ6M,CQ#d#Er}^JqYӉ[G]G,*E(C7^¿PTk{1 z˩|l [LYu K" Fn͉fj.$h, <9;q(Iu>.cĢq'ȅEa():k9qkw 6u1˛9QD'[tMPa)7 > ߗ[1Oz_'iP((8M}!+:n*]Pr 2;LJl6z^K7Cژؕ)|G:l F\YeZqE˖gkzQ|y79S\Zafs]ŖPZzJ+_fM;ENO?e>E hp!.{-BP˧)ãx79'a ̼1GCo;}kuUlZ{we۷o7cM춵Ӄw7?ވ!N{Y[PGPN% F#U~Pp- Vj, GERZT3uQX3?!CnR֒@"s;h>L~'CFN5֏{pEzKUNM8x`śwj-r78KI1N3'NXBF#Wv]=ڦ|d c%+ V I)|'kKFx跎 Zxc8Ǩj0SL SϑEMPҬҠ$KPtHeZsp4DW/ / / 4pK]/ w4(Z4ԧ9a0 ܕ ! 4eC n8`cA*A Y0_, _hP8` )q";Jsf3͂-Mybo؊'m_|1bgJ lmaq`+\ĝF}tY /eE 5ӫ`IYxh"}z ]s?==)?Όg011ś1U'TዛdivQRO_U7y2<#E]tbƻb^w`U3kZQS. e!G 9*r& k"BL(NJxKe1AVߋZyqhԚ"b9UsF#b'jp7d)sWNuy,Q_E15Ki!~?pNnU, Xq?cÝPP롆tUQz.%7|"kW@ZI Xd@5MR[~89mՎ*ךp%3jiCUcGk:.xV">R򄧅cߛm]N|gFi+-Uiyr{`(ܙ:\niH\s_PXn'7b?I.Oo$-}3@lZc xCR{ը.JQm#] lt/28)3Qahid (YeO]^+g@ȦY#k'>fT" R5"P:,F9L?vrNYlJD.⽭[hϋ«8\U(<Zi-{;I9 "qЇq~2m*  W*,ۄ-4I6 RqwLx-zo)kA)]aip9ES&dzS|QȠED)Wj Pߏ|$ Dž=DGkR1J}s)~X 'i9>֪GnR47ӡ8i jRFm}aC- xb 2,IEXLA|vQ*Jhz H:7];+oyJx,0( >#7nS?NP]| FkMQ4)Xkٴߍ_f}qYJ`9@@ͽi%n9&,U IkŧlE[Sͳ*KsłUf&a? U"ھ0\'c汔(SJsv՞γt(єάGI;bUhTz[ L'`V;Wz'$*qqq0 ݝFc;`jH `i)ZID!5 0H!nA@K #$$@@;*g|d[HvF즻!nnnnN+9Pz9ñkk^'=}~~-;j6rp `$,7ǛO6m9#?qʓg+nAJW-tU-6 }JbB lA5h6~5V1øAԌi544^͠fX3ٯ9шژE+kTZٛPA$J La%.*! d|#|J\qq`rq:(5 2|x.Hǃ, F'o$I<'~%t_6JH!zJniu^I@b-JLIU`gBX#A9͑bEvD@$ˎSi=:(o/UB1%sT}O"tJ>x9.evbaK60Rn3:aSa+H:GR:ݛYWw([dddwdjÇA:w^6MMMMMx`~(`cGe€i^ 1E .>g>}_L/LL>So$*; {ՑV:ETWIjS6:HSj5"!95DOj:=>47mr>>O Iȣ`SkA5 ^mvLj'8eٖp.k*J d<' =?%~"gt4%# rD|g <^=G'zA 9-& "# 3cb+a5+Ck ? XJ|8a' a .S&G&q[D_$yZtbYbq(r8X"LkiV2f9OB 3Ђ8V:Ҟ~Y\2I]UڧWHXۆL[k|_uF]NT 8|~G|rj@ !VOHބCf݋ڒӣmd};뽰oiwrl%^K~HмKj&ݤ{q Ivc}%ϥ₣[=v]wk?)j7:4$vtvk<˟+Z{Q_d%rIQ86pwe;tmG?f(lmllilH_6:f δr&L REm*p3mWڅ*PpQ*7*l.6 5@}FCq%{\s?J̯Sb GV?I3wlħRj`k]ɑ><×B+Vthur#ITKvϞ)u}}Lyf4џ~鋺7uy]rR=׵׵5_}^Ց~_Ph_{a zmfe<"abFO gUeke+”YfSYg=OЇHImds]}aLm N% s@— 1Fsd[o~w^SNZq Gߠ)M{>)w?8+57jz{NS'S)sitۥ<9J*F )ۣ۠)5 %ǃhhH۶# (J/K]J endstream endobj 308 0 obj 11467 endobj 309 0 obj << /Type /FontDescriptor /Ascent 891 /CapHeight 792 /Descent -216 /Flags 32 /FontBBox [8 -216 991 678] /FontName /BRVQTP+TimesNewRomanPS-BoldMT /ItalicAngle 0 /StemV 0 /Leading 42 /MaxWidth 2000 /XHeight 594 /FontFile2 307 0 R >> endobj 310 0 obj [ 250 0 0 0 0 0 0 278 0 0 0 0 0 0 250 0 0 0 0 0 0 0 0 0 0 0 0 333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 722 0 0 722 0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 444 0 500 556 278 0 0 0 0 0 500 0 0 0 389 333 556 0 0 0 500 ] endobj 15 0 obj << /Type /Font /Subtype /TrueType /BaseFont /BRVQTP+TimesNewRomanPS-BoldMT /FontDescriptor 309 0 R /Widths 310 0 R /FirstChar 32 /LastChar 121 /Encoding /MacRomanEncoding >> endobj 311 0 obj << /Length 312 0 R /Length1 28160 /Filter /FlateDecode >> stream xսy?^s}s},"*!xq(D"*"(JG\0>QcQGD߻jf9LcO}T/-0k{`1J^#\j5E\tIHנͦˮ\9p9[?s:r/QP&d..Ix7ayWϟĜ 2nlQ^;!j҅݉kNk^\z6oon57xs Q?2EVs DdDPBo0+Yr{>  Gx"JWUg|M-ohl"ͤuQcv?abSΛz]*/,5IfaCd6%9FD-d5irDP[tL7З>'Y 1-}IR m cOһd"D~/n#QYEC,z;ڼi[TMwFBtq&m-7HNX# xxS@r" Ւxk7*r]*=d&}l%0MH/u=Z@Tҵdbhg}G1[fUv{7H/'J?\^]yhw+\F_J _> |PT Q28+5薴r z=ʔjBNV^\w>Sl9} WIY 0wD~\MS.U<% fI+hғ¸ꦤGOl&.mX}딜?YR{906UVG)l xMLUHsBf}h˦mzv h G^Srr儅7N^n*M춺)Ud̎ZB_vR" i1R:iPƫgk(d\vN(džbQhHC.&%RmLҿzE -"/e^_;|{*^9ѦMڑrmZUGڸD27`8P)0? 'ٜRg^u8mϠs QT_qjn9^ݗh!0 H c3*՚8c{S6lM/Y͗F fiuc;o獣Ymζҩ=hݗ;*~שrNӞP%Ą9aW>Ȭ%x݌ O7B@mvjJ$E43,@'j֬QeCqtWfqZ|gW/-)oЇů!;”#Z[jш lJJ65r N626gMXF8Si*_K=K#eVJpחp5CK#<"g3*SBRjNQ4苟V(t BeB /yQAkJ*]&Q} -zQGNWJU<(+nS V$^Q֨,/%+[ J9ݿ״2(^ۍpgɦ$v͡c[1~'κL`qXK*KUPx[U| T˓`\x)Nji\{p:,->/VIA Q#UB\Y#ђڷ-E-uj:]68Ge^b\[`~Hb "ۥ٥Iwz4=(=*{?1~=RXE1rp D8&&!Ah`yA-eoy?|??ߩ_'᛹t>t~18N%BįeJ mp D>cCwT$jz7u ǻ{B3r$ӟTgȣ0P#^bA?MhYxx<;kVMŐx מ{ ! r*5j$"׹Z{l cʇdۤm}8뷛vw+v+wvK?v(DS<'uX:,sg5($SX F~QH#v~6L 髙^n.~G. $BzVEξqިe!.RV ?VJ@ !uݵ%kJp/r>& c֪zW#X=oE%e(H ram]::ΐAMįUwpvo`x^I?G 򄏉qEsWY0o~ݔGGF/y#WNӱ&it1lB!8,b֨)b j_7Ea J)gYM!XDU%Ca"OGCoϒJItP4-*>cMTzj=]Fd>N&q%GY zikKY?PFYK$E0%[?k {S\ k|לּQ0(=/\$0\d4`uVjZX˰Sar : 5kJYυ0)lF.@!^{*R y蒔g% EUt ` jt͂+naoea5pKjEt>U{aV-T>Qlǒ1Ǣ὾ K47_<^\Jv !sQaPG@ՌӫNZtiT|:QT ~$܆]nj麀~9ƋElrl҆IضzĩD4@ /F5yJhkGR3:uLB91JRIEr89IM@y2iEҖe81癫+=z o.>^ߜCYìI+a$뻛A Fsm o{צ!Gbbt-rAђ yqc⯋)\|)Лe~σn.Pc4ZuߏbDݝӘ+ V|ɒ-|0Gb<7nˊ =<>lK2:RHsSps(/I8ۜW׎UvEWkDL;#XBf7+I?tCNL˞%6ݿ>]ȥROC@o5ǣ'2mҌy V@S yb/ξD70\I$]2H OWO =>'5I )bIXf%.]\\%J|?uX:͒26%5ULrE}KTD_1 sD#&Xf _dAX%'E3aQ$%Am{>} Y~b!JO&A?r7\钫xg]ȎG[qnۦ I&0SVV sK2>ڗ}z(|dZܽ-J+:̽rEQrrU*MNrAM _2^ta&I:19h G.єaL D?zy={uU^"ΐaPJ 1u\u(H5V7Dndx7ˡ@1h``u.,a(J *X2p,5jR*WC`{#tC)pMp( E"gX`T9!MOq,`>YzOα--ޔ]m+X̓ljx,69ب=T^G|oS>ǁ[>Z_^aGWJ*b9Y赆:+y`ו#qb71ewFfes/s;g'%:шOz=H괦]S!Q3La~P^oH-p`w22]Ðfkle+wL IPhI2֖K4 gZ+7]ďL#Q?ѫF}wU Kǁ _B g'X^ R#'f1bq1s&6NPi- {5 37j&cF`Qz("$LL8tMA_TMzJ͝i,Oz ch# WX|eHSJ[m݃uB ‹6kzFH}Q뭲<GPj[z(]T4ַFs|3|lxtl5 'eKjx%,ՔAbo% v]XNEz?h0mо4'R֣t:xT O!KlCC2^~Ằ?z!uP.:R] 3ွ R\@GoH@_V惦@zm.6:Fjtn0E&b/F JY :fk\ߒ5YkbĶ(Q3:5kSji[.8ظ\r߳PISe9.ـ`M nm!.W Fss%%jjQs ݬW)_%2]H#X ݑ9hBn؊|KsTaMgyA}k;fB/c>++-\3 4? ů9Y<݅$zm?qtY{@1\nHUr5 -U-&eean]h]Y]a]Y]n[B{~EР@ĺέjl#/SOƖz,2™¬C6$x8WwÔrUyI_} 1m(?.ݒG.;7n|sV5g{{<{*.w37flkNz#|P+Fi/DžaB6_TZQJգT)((aT% ezC}&RݚR 285$F^3D-, (ƀ;0JBw t)^؛@Ҋc5~EF0J,ïmBCXP Z~Ǧ7ޜM6@s34c&onej\F.2yzUrႅ;-AtǂAOjvqw5_4/hhVmIkMDc˅Z68߲AùGahΦ1Z9x*Bjl|Uj>3VԷ@mԤEu ؗ{-oZm_m^aXoh^׺λβ$:1'ꉻl UV$#:v/|%oV6byqIf3Q͌Ԑ){0dJPe3 < 9- ǒ QAVz?ɖן?' `:(Uuw[fo3T5|jgln伹<c̽&&MixSbڒ|-p<8QnAuYM*9bݣFcU? U˪SYLJyЌ2yf}Je~ǵfgG 3W]ő2"֛JG ˂kX 炯*B8*.FF^A]hOmmŏĊD$3ЉzŃ&? Tb,rh.nvk\`]\nYnΩ4f.z=AE_@M2BW_f#+cڔѠOIJOi= vfQcsB80aXI9#-8£3RVGIwR7.9l8nxwG*!6b&۩^B/#}?~pb<` 2]p5+0`/| d0ĕn:)9?յCq`&z6&DKQ}V{tq٦UkL*9Sز{0h|/ӒHgkLU™ <3 njuZg 넂L6/ +`ƛRQݓ7IQ>S!8+)l&A@X+&Z'fZЇGi=-u>j Qa|*D? 1S7#/FoP`/+v׷jfA-gog*T|#թD#c F;EyݡzEiA6i{C"U 7\v.(yAbr {NMqA`Fapa%k )Y຀g = {~DTOոX&u?@=1#H7 `3q-Kn=A5L& -I( t2Zh Q A[1ҖL è-\[oG'RܙIB$nN{UO&8dU8 Y}5Ýxb\8V9,f{ `xm\7 ̣ţ%sD=6%(n 0aNV4P[uʂKz@7)"BkKtj/}]+ 4;uGFJp Vp*e˹wUNm/w4G66~oBl ~M!?>^49OU|* E']Pvr&$EOyAOdGu`IexD^:tbX`2*,jr*q΍@,E3|z\jL9#ZI]QlƤt#_h<9g# A6VHόVzlXVܙZch1X6޳ [e)o(Z2/I"x. y>Bf rbI:o+W2]֡O[- om!ZzG Ďw[2*=:]NEIgBC{T}m|f\l:dc.IIbx\ϔ}ϗ3F sSGK;9pYͶWXrK?k^9S^y?gnqɸxf_8eGˮDZ{$\ڇ[ըJڻm@a_;lIg9$w"f XIȑדp&H| Dw`rpV(I0ݤ!9XZĉ WZ6|+PPeW)XЎ MCdZ`9Gcܗ-2$VM{ݾ'?qSWJſXjn*Vk;7w#ŁS]ئI\^p/MV.ZK`1C OnNh\ZܽFYvk:1~(% Ynjul⇢ ]xl[cfyh _(øs=,0s!/UnbY1/V|wPo'=(ΕlHė;瑈Y󝷃~jgSwfI=pLFvD0߇xu ā_ hQ5k DebX! гI^5HLa+)Yzf[.P~!j.lj{ce!3؋HriQ3+z ^Fp6y0ڇVMefغQï.RwaM"vʥ={7ܾ'7=cNNu|'k2 @K馍!yElNu~6, %D-vp%QX"pny'(hoٹ46׆. lΈ-sɛ;]X|b'\f;V'NNg}m;V^<oQ$,V;Q> Gox`c!q4ׂ4ZԲ;*qGJL;ӈô䥖`B8 ¯nl$M;O[k%)Vk J6ǜLv5XY2[A$ `3D"9h|Y4]=#AXn<[Rb3۪ުY]z cpŴ0):ؗ \dVDdi+њ5 T1:#4FSAk K;FΉ} r5c%s \e69Ǫ~}4flaa x1/=-^6l0ĽN;[%ZҿhWk:e #Cwwww b\@7pp_ZI耍B.Gc#>eɼ%¢mG>^> ),{y`/췋SoS}sX 8Bat_t0R U1 pia } ćoIذ"{@WNصsp!;7p>(>S N/dcvL.C 7J Ylᕧx Ubl\.<d≚fؚDJ++Wm=q Ft뼎ǎ;;g7=n`>0!=vc?$:1؛ LV܇Pׇ ?E_A<xٶj$O0gIh39 bVr׃+&әi/ܗYW5KSS~|9bUbZ-_>)40CIv@݇R x[3,qYi v$x"UdӸi]_ƇDW C@>?9L7nDG9E^nBW[a0DeuYd\, aQ9>WL\pٜ˵\,hFb頸 w+@ wN ]S|ﱺT"?W N!ƨjcȸw*Q!&ߊs1wä;X⁐^/Bppj? I9woLIb}q+J~O挀FQ^ /D#Z!@+peK @JV#݃ܒ,V`̀4=Cf36>weQLXo-)VK<8ES8S K:1*@hsMŃ\&q\bX%܊l>:kh:4m cr913n-XmՇT9 XÒ`HY Z(Zf|aaܡnF r"gFNKX\Xȱ7rk P06ak%.W=SƍqK߃{;/;r/a -ܵx<7YĢTSUjppGjl6\K>_8. '+`1n߾#$ɤSOo 3G̢~ᔙ1dgO7.6m=UY"m%z+oX,}.Y ) c 5;ǀf0Rp h:#kiL@3\`80m%Lf3̡̏QkK?@-ŵG+$ 4ID|qֆYF)dxr1x)xx~:uHˏd 0^5S><5m)n8J,,27x}Q\U3X}V1kc}JX]^&M`8Fؖ[q0曁P,ao[Á _odmy]O"-|hY= f[c蕘cfeHZeЊqf6 51P[f(M1WwĦ@xWU ᓠ,C[\ wd"ػO9ZbckM_%b!=kt:+@e m!=cv1P,*WG܀ "t]ȗLɌdB2`Y1Ѝtd͘fЊy5{=f‰vN-62й-'&lU"1W㘭~|H 5@;FP!y/!t#S{c,)bz"go,9h`8HRM 7X;ԓjhpxx1jS xvVŪӮlRs!l݉vO` WqWfVNLj:F2]<ӱYJ5ZD>DЂN3jc$Wha[-`N|a\%)+, R _+~Z$ S:G!SyǮDLzTn*=۴s QvO}|<<[)*BݱXHWnaiM456Ag~)ـzD+kaȽ\O݌1f|3scVsg@J|'*Ώ<}zsb4䦵4_\;FZi'gHߑg#wgމ-v8OCb QW)wncH' ~i~!JTKC> L-OAqr*ZR}=uZI{BwcQHqCpqqicsQK/֧mi3c7Nyt\ ݞzާ}aߣk <.bpmxogIѾ5|bbөiKzRU 8̡ވ$3@D?mY:9[gLkpԘW\wpӕܷsp$ԩpmd3ӛnE;!E<|%dF1ٺ| | ˁ30;Yw:bo$V 02ڡ1vM:7LTGW h s׍hvmHDZt$)8ٚ&m_RчHi-/yxD%Uae4ɯDXD#gzFx-k)O}22)'ƨd۱H]H+ypP;2~į,̟oDl04@˨װHV$Ù=/_ˋ3H>ZH>抸ϠSy&eę'~#0zϏE][wyˋ^6/y XކWҜA{c3O$oHDk1k+~"yĂWyˋ2v餗Q/xВp5fcoI܁^>gXi2V#f0zՈ8d%"J>L.?qCANz^x>{O;~Bw`5";)Ո(v{#Vx^aѢl ؛5xCGIo#zwieb endstream endobj 312 0 obj 19584 endobj 313 0 obj << /Type /FontDescriptor /Ascent 754 /CapHeight 670 /Descent -246 /Flags 33 /FontBBox [-22 -236 625 705] /FontName /VRAWYE+Courier /ItalicAngle 0 /StemV 0 /MaxWidth 823 /XHeight 503 /FontFile2 311 0 R >> endobj 314 0 obj [ 600 600 0 600 600 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 600 600 600 600 0 600 0 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 600 0 600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 600 0 600 600 ] endobj 44 0 obj << /Type /Font /Subtype /TrueType /BaseFont /VRAWYE+Courier /FontDescriptor 313 0 R /Widths 314 0 R /FirstChar 32 /LastChar 211 /Encoding /MacRomanEncoding >> endobj 315 0 obj << /Length 316 0 R /Length1 38056 /Filter /FlateDecode >> stream x `Tչ8~νw}̝}&df$!$,%E$"%D[|KkX ѧ.Tlm؇֪E-d3,swa !]f r^aTH8BVb]CjK qp*O8RTH4&!='%3 +!;rL;;q"r#"C(C("(b*CIT*P%J6BF<'SPpi(ka艠h8ΠaP@?Bsћ &:<](AȎh;zG( 6ts HbVz8 | 9^Q5=*t:ނ~K5.C̖G%@L:-Co/IFOɽFqxm͆Ӌ[`q8ށQ ߅]g);ƾQ(ВBj-Eka }wz8+`D_139py#{#;pYdq esaF,۠ |) '|Y 9d{؛b rq)+V*V,Ul+s[}I(j~V4]c]Fm/w1U;>Vu7؎/C9 qidVf s#?b30"v ;]qWTT1Oqau!..TqjrlgTp*PsqIq?B:4 0 $@׿c+8Pֈp,R B-x_o?wۃ|'Sp?Ɵ3 abLHL;skup3XAF v%;ngȾcr.5pK5ܛ[¯hQUܯxwY?%BppPTFZƽ&Ja62^8~0Nr678+{ {Yl+5/aA֯gW[Q?|Ȝ`Yq'nO2fRzo9+w@f<^^_|+(gBw1f.anA\Vq]b#,f\ƾݏ>bC?q|PCxf.`1S؇&w"?]x!ct#t Y5!Ӂw0Ǚ.0o5it%`д&U}z ؊#[`y(zQ=GptPz `ffA*~2h_RX}O 5m ,fM8Gʵe{ P/CwE W*PtPoz+^K@ Mu̠Y$PQ.CsB8^vn70'#M"yxAjq#>euڑ9xFjX]Qv5@nߍ00 R`cМ1g^fHUcvq=cЅc(4{̐f1yL.Ѡ[#¾vUhؕF˘1 ߝ3f"4nj>iq\"GFv;@Z6ƘHkH+|VX|{ 8ȘJ8Bj.T١#¸Fy{\.`(rH#ݡX;ԳrgiS߾SQ[4u0\ S^Gsqk;̼bң1" z1Ցuh:~*XKT}#b=ԋ0D2?w2WDZC-T>Y7 fiS/;)tkI[\j\P,}Ju 0^Ƌ[41'GU،o= ԉi\62ֽV{##!uodxq$FlH P=zk e- g!|e|eE;0iCi/y1Y `< ڣfs {g"@21r*yptM@*|9W S 'M5FȋIc.áS;qJV Lw1,,`}Ќ]ƇMڭFF}ʈM!j>c~yLTCߘlǽEj+0X2TlcW6MiEelҟV`6˗>jENVO˳&`?A6]U !%!+%Fj֢5am}d[4{vl~^fv8 *koff7gg*q5f(=9 zy ws/^>Ξ+ykLksɝvnZ3erAg1koL&=xI)5IXqRw#ؔ' <$:,ꁚ4ogiHϝ:eϨog*bJc$Gt93GmOq^Ь6y_9.H%FHE<1y=^PЋiIa=-B(ڄa=F?L@NKiY=L& y}>Jvlۦdj}Q}O(ZFL)8ld|@>}~t2qxUj)*gKVAlg{}7|.%Yߡ?gKPt:\H,7 Tð2;ܫt$_J:8ʼny7 4?ρ3{{pBhjla^}dg\XrV|⩿_wcr矼sW(_uLx ,輈>+12̷Zcq}sJ[mj{GĎ\8罆55h<,Iڳr.N_JfD߫("paUw,>[+Z KqsY.(" / WҮĊ+;Sϰ>w^憎S}B_z*wo}57Uտz/Y_agwABaĂ"` s9-mLe@hG h2$C{`/a<]\_W!#L2;ʱTYaߒ#tk rX6LA"Eg[`Q_ v a%dâIށd@sAa+ddOGAIr3I LRҔʆ\nh8DqGL5lg٘;QӊjU- XaJ|D_`@ 2Ƈ* F"r*GlQ b! y^`uM UwEآjf.릮s^|jU-yq۬n77/> 7Pq Ee;Ӎwtr`:yʗ)&Ŧw~jIF֍#wgfUZV1y`V1 RVg?F^Eq`86ߦۡctlJNYT *'ol42F j;Ơ̈̄\EW R h65pǍ"R*]^BDmޭ@Z+\NEVu Y9ʷaz9D'0jv I1MjX)r[~~o{nxmuӅ'p[mqi<ㅿN;g3%4|r' `VѸ5ޛ;߉ ˰xyU޷Ehx}`î}L\ C~.^cq )= &LI2MIa/*Y99  ⣝⣝N$JE^S ^P Hq V1b %e_f iiwۨ!d$ | Zfmq| W~˒OpUǗ] \^E*m޾Ѓ[sd1({ʝjq\qFtNFkj6*΍eQc̩Hm kXGMVNޤa)}~AmR {͙{GxP~?YOf4;L&`j71d&đ%0GpZ֨:G ePѨHGV0Idd h mTǨA<+ӫcXY@  ,ETW ].T1k" Bݾ˒ԁ JA%pa%\2 X^j5| jKэ MPۭM媣[ yc{|}uuf$yՇ~ wF#Սh4rVgk5TMF7?zA^d2o9sVW(n&tݦ@w@Hw!ad9dʝyT+GlQe]PVJ</~ %VC6؉~fd a`JF]R.E@n:H`Y&qtgԡSi%8ԩd:dk&͗͟d&0GiHi;uqCPƥL<3X:Hmr܍Mx ̡94[IutMC$J@i9g.X^L#hvtwPzgXWU8\5ihW_JG2c_Ծ;w9>JKwJ;2~2*@;a r8v:DNY{:}N;R*Jcqez9@3UX_U!MDd0%C֐ȀD~K2 |H )(rZVLMky9 ^0th6E;ƼC4ґiK6h#g AEOcocu;ґ.@o-ŠT|RDzi6%w eHVd7,{chș#'o[)ޅ(_FLش ў`1\gh)294AX3w Ɇ,d]C&3LL87{U*̸]J5-6%1-"+3@>$C٢xJXJ &a%  ›ABv&t)G@J,UⓓåeF`%DPP`sWnDyS;Z`Z֡KMט~ ~_WpAI< q1]{}Fưצkf*ٓ'=Ӊ8@Hl0M6Sp:fۣC3K-yF6 '\Ua)zCY07ggG,O}m"4c S? *'rsN={>RnVab(|[ rn.#"I7 \RG5Ϋ ~6łZdQf|/YaWcN^klU1`ɜ-dHH~E2&?0AUa!^k@nr6D; drt%A#`Z[Cd+H8C=  @'rmW}[vZho?La^p&|c7!znr >ڱ޽s]Ndݿ12Gt%ʡJ+< O?w+vkwm0C޸ROiB !T^7CL($X@ 2J.tZRW%UbrQ9Qy,TTTA⛞V) _Q97@dP7w %3K:`)7XZR<ģ ;{a\bβjq0 $> @\'̀%#L7WbC B&;zVJZ.N=]$bq8v7Uf)wwn`-\=/DIm5U꾻1 Ne!x # ;ibӍїC/WW0ru\ H4L"dkWI##xX"OF(t5|CgC'*8kEBUŘⰾ%)uX`j"tE<@;'O9|&̫xT_A-Ke۽Rr͓B~"PU%{0&$bӑ^poQA_~3,Z:H\*oȼAc@ DSU*+I.)JbE{e3ut> b,B%$-B2n;wZ%0h 6"HX/&Σ naS$ع VMD-i7>Zݑ;?NK-Ĝ ; 7(bH`7>||ɘ6ƒS\~&&>~A1Sx?S5a?=36A 6SOqjMb۔#6L% GX+S4BNاO8w=8@}DxMca0j6P\b]e_mRlKKrG\}Ǫ]X+@zEOqe)@ (t8S沕X#Cty J!+Nʝϻe R`7۠W~/FKJA CP ~ZxI;9mm6I*[;cVdC\ gg>;  !^c1 ? NAYC}N/|ՃR;Lylʃ?b^4At'嚸6nx,S]\~Ц仅ne[Nwf.W kֺֺ/m75jumtolNݪܜ95s]C]3.壪G5v<}8Wث|R=ڗejNy)-X86Uչ/_Y],\\bT m)ǽ4un:4,'@F㱥< FkTpEiukHB\(U*/Ox@6S wܤ5M1_gb^F-ɖRMnjj r{“*Ab+L2 ;t)ZT Sth#QpRi"Mg2="ӗ駅eUi] kZVpqҽ%TȸcNC:AUa%&"Ža}QNLlo8J8ZE4 20xO>rp;LFQMz!qdHiFiL ͹>KpS,!…+*+Gk>K˙Nsu akfC7|OߵG;%0Ã˫fICUz˔캴48nez ebȷʈR!6a&vAtU#P# sB]M 4BWVVAZ%W#Ҏߖ}!ZO8ĈOh(P B`={CHo'HUrbd%Cąi<#Ռiq 6IP.5kJ67[4xV/65r*BBEj .zj |ܦkm?sfF 4N6߸SVlCe"4iTym\aM>'|3D ZN Fʄ&XX`S- 0t+*8*9MD苠jVğ໪kR$_QF89r< EpD$Ef!$ CN3K` 4ppYWԨ$nKE| )O# Ա^qrqr.d2U"o{X-s{HY.Oں:W)J 4y zd2:f(TJ8՘<pGdMb?>shF^nnbǔ\+-1]gk!$W*>Ő LkٰV+:tQ71tNҥ!;;tO6t}J8@ B66CqT%k%8"LoF>`%jl H !}#PDJSC)ledKٙKXJժ窗.ۙ&RǓ$!);]@Z\FځvH W4Q9m2sޘ΁_Jhr+59./?plنlOX?hڌD}̒ɹ9y˴{a r RJ(]$Eʄ\BDerJ]S+0]xxig%lWrFblbC :ojJ갱+4|Ec/_:/㹋 %'`xӟo_Bay0WڀiIh(o $zJ4ִAp[}^l$~4Q@Xbiyd 7 ړF DH,XCXr7 kB;$ 2bK(}QIZaĢ clq wp큰XC#%n}0NB*6OB8<%0WѾBO eه, a͓A8~ yAV'lYZ2杪ne.*^P) |~+s+?oy̱6=l@de?nWp;Awê) jT\UT];򷚷:z\_5%gڢ<,3@R2w8jf΂6+VXXwX?[U A jS2Ts|Pٶض2DFLZM2o+ W>rh++\6$FEzQ/z'=Q\} (2 $Ht(` {$nPH2`3`. #}ܽG2CUx$D|Mk劼N-OI.ܥ{%u.SђWBtJƼNa_OO/yI\  ,Z5 {o}ejR΋j_xc6Z~aGu@&jk$HX2+v;J}+ &OeAPLA+vC8v1d8x%LiB|I| 4XAd@[&0L0 1i7L| e-ϺEnes77jr8U%0ʏ ;;T*T"tjEي$Qw>0*eȷw:_.pٜYy~hf=37zػlwGװB\l(۰Ae|%-X۾|]۸m78Ff]߲;w:sf_mX~xǗ]ZYj+4 ,p#g72-*%Lb%2G^#)4;"ODq|(\#Ϛv@( +xVv. d]{Yaqx2FhF݀Y\N9qs8e~;U"fd5/8Z c|pͣElZj]z[zGM;Mk KP'$cnC}"|֐G C:#8 .=sq=yn" l"ijiyz|'̹b9yգfՊ[-5#ٰo{xﲑgFgeFܘI,UeX2No*'O2O1,[ވX{+=J6,?z&Pڒ2 A@l(NX_2E<  |`Ow2Df /Q݁␓zN[i[Zyv@U:Ͱ|S=Z).tzgk-l(FXA%;%<]d gn[f ,IAJ ;ʡ%C6LN%>$Dn!IVWXXVxp,@дEA$ƙ!Z<٠Ak[ @(a$0`\C;{1gykH~-Ҟ_]w}'_Mc +<.X5'۱–שtg=VC|N=7$+xݥVXyQ8V7pKjlE?t3fDz٧WIgm8`>Wl:`z6)#T;¤n{0~MА9*< =18`OP ds Z0(6Fx_H-df+Ȕp 2zmvm`{&23g erԖA1\8MPNk$J8^! |W^ዱ,4J4Ԕ)USvѪiZO`4ǩ2ɈO=IxEE]4צL{:Zd@ T\SlCuuXD:6㎺~R%aIHg/ N@gre(lrsAt VFxL ~j@^*Q|>E4bD "`u}Tz;F>țA|^* O$ > :@^I;A2MM쬭db3 AL!RC::Y  RC=}P!' 7awR#&*KӺR NZEh*W:]Y@.3ZĜM/0GԚGa(yZlRBT[=9Ȇ{Vs9/>;w:v:;==?@ *ȫTpJv %^2 3pM>e3f53Thntc6rlF!0%- ;%!Olŋ xbm4M5q3U~'M3!S=%-AhM: |N2 #~'bbq*asZ!b9T"@Nf^¼'\7t=@dGr4K˻3KPJ"N.e<&5|9N2$R9RB+Ʊ!|ٰ™񫡬V&vp n1@oA_)XI"KiOYYuNߙP|?[@X(M¥yS2q7x ~@m!@/ved@N<Аݙm\pTھmvY Zٱ١PNXYz;s4 a}PY%:Y<La0z T}Jy?Ԕd&I]VO}Wg+Fc>"T'(ք2g/ 5NMt 4krAqU"GZv9R{.@O',xؚ%\=U5v,;}~}'<(h>aA'||,tڍ]x5a|6CRٕ-kG;ǵ YX(P,lsTpH:s .0(n;ٴK)+x.q!F#DA?]v:{C#v#]ZcB,Il. dS)=Ejyin}JZx=Ȕ$DȔ̿d'j$}=I_O’Bh(Z%o4޴C2Z+iH06ןڐ(XjC*#m@=YC Z>0 H6ge!t.<]뺶t]KG\~Dx ĩ wZb%P]U)OgAX`O"9Inu:\#xDRIZm&ZjZbIyJ4CP2wkka Akzwži#t\E9=a\ R*2G(@:בFs 4??;cr/7ȱ^-3D^T&|!o*)řXYSڗAj:PBv[XU1%6sfM%$J1$7\Ot$ n(1K(!]UlI1xO169,E݃uui;{ǠVLZ1UiEĀjmK6&30,WE٠Tf@#0qŶ% 6-;>lh<\ w>8E 6+NK LG} TԊ$\Qw $#I%G(F@# Uo*r<"/ L8 8G 8H$Js߽ޓyg&3LLHL QDT` V5sP[!LԚp}{NZ=ŖZ 53ɷ{5{^[RF60dDTFEis'0ן~]F$u{{-kR W$ҥ[h(){U4u{U!>դJ:Gqj%'k'{p:"IZ┹j◉[WdI@2xdΧCLj򂔍XePӧT7F=={wG#U:FUV[!#]R6JC%xs|WF_?Z%qe? c?j/>륹_w?7HׯϭC_sגFlN<:|,$l vL! l6"PђDʮ2kt[p H&jnhfGtN5Bڈ/7V#ǹ#D'Y+[:olt-t`)”Q*1aFc 5.Z"B*GQm H\_2ot&oZq3wӁU16?FiP5VKWGkA9JQ`gIb:3 зЬ9[kSOb뇆T$^G踗ԧ}in(Bz^gqClZBI<9ʏ541DC y={1ݟD8Dž1ט,0VnmfcD ƇcqAsDrD>R.XMmEG4Q!  p6OF RLxtr BY?lBڍ2Gl^pNT~?y^tysy}~P8@W}B=5&h+P`=ݷ8? 䜒1yfՓP.J8{d c1888A@D@@ĜȧV@[g Od.AW ]M+}`x|"t-Ȭǖ:OR_z_H&nܔro]C2.|(__W8`hkObm#;gv&'DUYw֫=D0{ljf->J:î) X4}/O%T?ŁN[VtA6xP]˷4@mtNDk^i߮dd9b!wXCJX1 -*eZ(R6ݐi#B&48q:CLUU^.-~B=h7FiH敼vu ̵>6G߮aH6p2~yo9wŜ@*&" ^QUqMLHthl/ Y@9迬)Ϫ{X `es1dj.}u꾢{DUsO=oCx9L͏y?M e*7TC&>,%Xz<^o1_Q|U]`!.S{m5V]˘53{r/:. 9h -~6?!R9`8gԀtbjqJ7+VE=;{`?519=ި|/嚨`4M^8f!c11DVt7|Xߦ^Ze@?y:q9|;jGE|>)VUDGA' ĖljhSFp%3V$CC9x(.Mҡ!Л$cIB䤂69XwszZ}/Bv{F ༳~SNy,z8f8r.,6P$8 ,?-P6d@K1j`zh UG{TM_/yN;_,ϗ\,vu -;VÁqS+衕'a4&25z6 tHh]ri6(`m3:]!6ˎ ^-uYC8X ƘE;1Ue1yz'u'tHp x<nid Ix򦢯M<(bs +఩4P+ V(&z,lOJju^12L,(bM`:KH1g+ S`y а(@P@t\v9zpuBIM.i:܅b 9JCkˏ\/\*2@TP%F(SɽpPL U Pn^^# gY^뭃}v8mW} CSǛr&'w6THeg-!ՓG/W;rd®7{CO@G6\7OI I̭ו]&T U*턇<#^nUaAuE[D oVyI5Ubި鰐2`ن yR' ?۳{jmw+gĠAÈr J0* 1=&Z &. @_Oxr3> %-VDM E?)zz jX>kidW;K7P  J: mDSikwBfOAN~iE^LX D1wApOիjcFhc.-,xb71*!DBHfKEB+zÅsnlsT ]|o7έ^x5o=#__=`i^w$p܋ s/.TuYy봡NnpgMtAS U0op׀2|5nR{Q=Ϊ@8 ZyWi\5T™ 8\Ru_{Ȅz&ŀ iFP'yLB5yN kq/ip3 \; U;4I.f`xuж }Um{nvlxc`3dkvMK[Znw8*jxYZ,TS#N|5V_7u۷ rn5|PZΆVhWXBŹZ.  &#-ue8? wUpku8]_]`)V*YnPx~͝7 ю!A?:;s4TD2wfU࿉BF8SrX/JKT͋ʁںؠ=~!9 (&$aL Q?&A<?r/?B˾_2rg$pstlq%{tt Orqa@YPM ΃7 SWxJ9:B Ǒ5ĩ?QO?+@KSnG? =|TzBKO o/xK0vOy|xwƓM] ev PfB]x*0$ wt$ptƄh^RBBgy'&s"7MBnaeL4u˔& z_R J])fiFUjU}]^l'6HJmeP? zի ¸ s+B0 )[mSh5p0i16'53Ӛej5C1>)?h2+64 נ@\+h3@,1v9bViW#fA9 YjA\  h.lT: :Ҁ5#H7#! L3iZh+zAK;z ܅J | Gσp) J3I2I{!Di+8DUۢz=ўDAU1\Ń*lSGAL)!T<򅕯T ܶrtuRK85Q-Kb0 \Tmp%i>ԧH+4 E/Rk}Mp Z:'e=pi o^  +:LJ!aJ؀u:i}&VL .s1m:k7S}{[dnD10Vټ<ܐbh7=ni7%M kv\ uoi221dCՐi{' 's %_b p"EɴN2Hvy nwX8 f]OJmIiGKm4+-wJmJ"cSy]]ҥ?V?. DsS?G'HKj庯7N(亴KY>XJR6x(m୥ZFӲK[R#nC|YR^A+&W ⴕ"aU'Kya*8kt2L,&RSN/XJIߵtI8}\%4N>&>![d\[f:dQ$t8}t #?D[?Az$xIT["/}퐴b0d+mqQj !htM"Ӵ)ǡcR-jA4myohlDIkwho^JXZ[u:άPN"l8gRtjM8 [14Hn7߽f ws,lIuOk篞\h]u}MLd&ӳyhEc{rf}<| /羳-گv g%v\&-ZzKkO~o2G}ݓȐm!?Nw^M=}.|ŰPGMq5WCXn5GjB%]wYY!^C1>Eؽ"|C(r͌YY-t$E}i8}돴DQHe(;̲>8EIzʷHˠT:![܃LmM[-ywrFY>g4Cb7nL,crsC>]WٛhpnS5lRt6*;;/{+GF]fWҺ`U]4 ZW7%wj1 x%nX <0mR}I7uu4 o'2#RY it3<̡aP*EkK@n3pAk@j_;W?Tܧ>'MꄺNZcx 6rSo*k6e=\-` ~ O rh3 s^"N 8+E1 8J #{W>(C!>L/C uo endstream endobj 316 0 obj 27999 endobj 317 0 obj << /Type /FontDescriptor /Ascent 891 /CapHeight 792 /Descent -216 /Flags 32 /FontBBox [-77 -216 936 695] /FontName /NRGYYP+TimesNewRomanPSMT /ItalicAngle 0 /StemV 0 /Leading 42 /MaxWidth 2000 /XHeight 594 /FontFile2 315 0 R >> endobj 318 0 obj [ 250 0 0 0 0 0 0 0 333 333 0 0 250 0 250 278 0 0 0 0 0 0 0 0 0 0 278 0 0 0 0 0 921 722 667 667 722 0 0 722 0 333 0 0 0 889 722 722 556 0 667 556 611 722 722 944 0 0 0 0 0 0 0 0 0 444 500 444 500 444 333 500 500 278 278 0 278 778 500 500 500 0 333 389 278 500 500 722 500 500 444 ] endobj 8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /NRGYYP+TimesNewRomanPSMT /FontDescriptor 317 0 R /Widths 318 0 R /FirstChar 32 /LastChar 122 /Encoding /MacRomanEncoding >> endobj 1 0 obj << /Title (Untitled) /Author (Anthony Brummett) /Creator (soffice) /Producer (Mac OS X 10.5.6 Quartz PDFContext) /CreationDate (D:20090514170054Z00'00') /ModDate (D:20090514170054Z00'00') >> endobj xref 0 319 0000000000 65535 f 0000645667 00000 n 0000000384 00000 n 0000581203 00000 n 0000000022 00000 n 0000000365 00000 n 0000000488 00000 n 0000001500 00000 n 0000645483 00000 n 0000000586 00000 n 0000001480 00000 n 0000002309 00000 n 0000001535 00000 n 0000002289 00000 n 0000002416 00000 n 0000595978 00000 n 0000000000 00000 n 0000583746 00000 n 0000002829 00000 n 0000002541 00000 n 0000002809 00000 n 0000002936 00000 n 0000003086 00000 n 0000040310 00000 n 0000040622 00000 n 0000040332 00000 n 0000040602 00000 n 0000040729 00000 n 0000040879 00000 n 0000112584 00000 n 0000112786 00000 n 0000112606 00000 n 0000112767 00000 n 0000112893 00000 n 0000112984 00000 n 0000215526 00000 n 0000215765 00000 n 0000215549 00000 n 0000215745 00000 n 0000215872 00000 n 0000216344 00000 n 0000215971 00000 n 0000216324 00000 n 0000216451 00000 n 0000616650 00000 n 0000217069 00000 n 0000216551 00000 n 0000217049 00000 n 0000217176 00000 n 0000217918 00000 n 0000581327 00000 n 0000217276 00000 n 0000217898 00000 n 0000218026 00000 n 0000219583 00000 n 0000218126 00000 n 0000219562 00000 n 0000219691 00000 n 0000220048 00000 n 0000219791 00000 n 0000220028 00000 n 0000220156 00000 n 0000220995 00000 n 0000220256 00000 n 0000220975 00000 n 0000221103 00000 n 0000221254 00000 n 0000249280 00000 n 0000250573 00000 n 0000249302 00000 n 0000250552 00000 n 0000250681 00000 n 0000251000 00000 n 0000250781 00000 n 0000250980 00000 n 0000251108 00000 n 0000251505 00000 n 0000251208 00000 n 0000251485 00000 n 0000251613 00000 n 0000252568 00000 n 0000251713 00000 n 0000252548 00000 n 0000252676 00000 n 0000252957 00000 n 0000581453 00000 n 0000252776 00000 n 0000252938 00000 n 0000253065 00000 n 0000253156 00000 n 0000362997 00000 n 0000363859 00000 n 0000363020 00000 n 0000363839 00000 n 0000363967 00000 n 0000364250 00000 n 0000364067 00000 n 0000364231 00000 n 0000364358 00000 n 0000364449 00000 n 0000541255 00000 n 0000541499 00000 n 0000541279 00000 n 0000541478 00000 n 0000541610 00000 n 0000542214 00000 n 0000541711 00000 n 0000542193 00000 n 0000542325 00000 n 0000542981 00000 n 0000542426 00000 n 0000542960 00000 n 0000543092 00000 n 0000544159 00000 n 0000543193 00000 n 0000544138 00000 n 0000544270 00000 n 0000545034 00000 n 0000544371 00000 n 0000545013 00000 n 0000545145 00000 n 0000545956 00000 n 0000581584 00000 n 0000545246 00000 n 0000545935 00000 n 0000546068 00000 n 0000547185 00000 n 0000546169 00000 n 0000547164 00000 n 0000547297 00000 n 0000547692 00000 n 0000547398 00000 n 0000547671 00000 n 0000547804 00000 n 0000548336 00000 n 0000547905 00000 n 0000548315 00000 n 0000548448 00000 n 0000549200 00000 n 0000548549 00000 n 0000549179 00000 n 0000549312 00000 n 0000550222 00000 n 0000549413 00000 n 0000550201 00000 n 0000550334 00000 n 0000551377 00000 n 0000550435 00000 n 0000551356 00000 n 0000551489 00000 n 0000551818 00000 n 0000551590 00000 n 0000551797 00000 n 0000551930 00000 n 0000552342 00000 n 0000581719 00000 n 0000552031 00000 n 0000552321 00000 n 0000552454 00000 n 0000552967 00000 n 0000552555 00000 n 0000552946 00000 n 0000553079 00000 n 0000553618 00000 n 0000553180 00000 n 0000553597 00000 n 0000553730 00000 n 0000554328 00000 n 0000553831 00000 n 0000554307 00000 n 0000554440 00000 n 0000555066 00000 n 0000554541 00000 n 0000555045 00000 n 0000555178 00000 n 0000555884 00000 n 0000555279 00000 n 0000555863 00000 n 0000555996 00000 n 0000556868 00000 n 0000556097 00000 n 0000556847 00000 n 0000556980 00000 n 0000557862 00000 n 0000557081 00000 n 0000557841 00000 n 0000557974 00000 n 0000558801 00000 n 0000581854 00000 n 0000558075 00000 n 0000558780 00000 n 0000558913 00000 n 0000559278 00000 n 0000559014 00000 n 0000559257 00000 n 0000559390 00000 n 0000560019 00000 n 0000559491 00000 n 0000559998 00000 n 0000560131 00000 n 0000561053 00000 n 0000560232 00000 n 0000561032 00000 n 0000561165 00000 n 0000561517 00000 n 0000561266 00000 n 0000561496 00000 n 0000561629 00000 n 0000562299 00000 n 0000561730 00000 n 0000562278 00000 n 0000562411 00000 n 0000563282 00000 n 0000562512 00000 n 0000563261 00000 n 0000563394 00000 n 0000564809 00000 n 0000563495 00000 n 0000564787 00000 n 0000564921 00000 n 0000565243 00000 n 0000581989 00000 n 0000565022 00000 n 0000565222 00000 n 0000565355 00000 n 0000566122 00000 n 0000565456 00000 n 0000566101 00000 n 0000566234 00000 n 0000567246 00000 n 0000566335 00000 n 0000567225 00000 n 0000567358 00000 n 0000567849 00000 n 0000567459 00000 n 0000567828 00000 n 0000567961 00000 n 0000568804 00000 n 0000568062 00000 n 0000568783 00000 n 0000568916 00000 n 0000569962 00000 n 0000569017 00000 n 0000569941 00000 n 0000570074 00000 n 0000571148 00000 n 0000570175 00000 n 0000571127 00000 n 0000571260 00000 n 0000571746 00000 n 0000571361 00000 n 0000571725 00000 n 0000571858 00000 n 0000572317 00000 n 0000582124 00000 n 0000571959 00000 n 0000572296 00000 n 0000572429 00000 n 0000572901 00000 n 0000572530 00000 n 0000572880 00000 n 0000573013 00000 n 0000573157 00000 n 0000573211 00000 n 0000574002 00000 n 0000573266 00000 n 0000573981 00000 n 0000574114 00000 n 0000575226 00000 n 0000574258 00000 n 0000575205 00000 n 0000575338 00000 n 0000576571 00000 n 0000575482 00000 n 0000576550 00000 n 0000576683 00000 n 0000577265 00000 n 0000576827 00000 n 0000577244 00000 n 0000577377 00000 n 0000578108 00000 n 0000577478 00000 n 0000578087 00000 n 0000578220 00000 n 0000578882 00000 n 0000578321 00000 n 0000578861 00000 n 0000578994 00000 n 0000579671 00000 n 0000582259 00000 n 0000579095 00000 n 0000579650 00000 n 0000579783 00000 n 0000580990 00000 n 0000579884 00000 n 0000580968 00000 n 0000581102 00000 n 0000582346 00000 n 0000582478 00000 n 0000582557 00000 n 0000582653 00000 n 0000582706 00000 n 0000583174 00000 n 0000583195 00000 n 0000583415 00000 n 0000583440 00000 n 0000583725 00000 n 0000583914 00000 n 0000595474 00000 n 0000595497 00000 n 0000595744 00000 n 0000596168 00000 n 0000615845 00000 n 0000615868 00000 n 0000616089 00000 n 0000616825 00000 n 0000644917 00000 n 0000644940 00000 n 0000645184 00000 n trailer << /Size 319 /Root 300 0 R /Info 1 0 R /ID [ ] >> startxref 645873 %%EOF Cookbook.pod000444023532023421 2743612121654173 16437 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::Cookbook - Recepies for getting things working =head1 Database Changes =head2 Synchronizing your classes to the database schema From under your application's Namespace directory, use the command-line tool ur update classes This will load all the data sources under the DataSource subdirectory of the Namespace, find out what has changed between the last time you ran update classes (possibly never) and now, save the current database schema information in the Namespace's MetaDB, and update the class definitions for any changed entities. =head2 Possible conflicts Avoid tables called 'type' or 'types'. It will conflict with the class metadata class names where their class names end in '::Type'. The 'ur update classes' tool will rename the class to 'YourNamespace::TypeTable' to avoid the conflict, while keeping the table_name the same. A table with multiple primary keys should not have one of them called 'id'. This will result in a conflict with the requirement that a class must have have a property called 'id' that uniquely identifies a member. =head1 Relationships Class relationships provide a way to describe how one class links to another. They are added to a class by creating a property that lists how the class' properties relate to each other. There are two basic kinds of relationships: forward and reverse, Forward relationships are used to model the has-a condition, where the primary class holds the ID of the related class's instance. Reverse relationships are used when the related class has a property pointing back to the primary class. They are usually used to model a has-many situation where the related class holds the ID of which primary class instance it is related to. =head2 Has-a (One-to-one) The container class/table has a foreign key pointing to a contained class/table as in table Container column type constraint ---------------------------------------- container_id Integer primary key value Varchar not null contained_id Integer references contained(contained_id) table Contained column type constraint ---------------------------------------- contained_id Integer primary key contained_value Varchar not null Adding a forward relationship involves creating a property where the 'is' is the name of the related class, and an 'id_by' indicating which property on the primary class provides the foreign key with the related class' ID. The class definition for the container would look like this: class TheNamespace::Container { table_name => 'container', id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, ], has_optional => [ contained_id => { is => 'Integer' }, contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; If there was a NOT NULL constraint on the contained_id column, then the contained_id and contained properties should go in the "has" section. And now for the contained class. We'll also include a reverse relationship pointing back to the container it's a part of. class TheNamespace::Contained { table_name => 'contained', id_by => [ contained_id => { is => 'Integer' }, ], has => [ container => { is => 'TheNamespace::Container', reverse_as => 'contained', is_many => 1 }, contained_value => { is => 'Varchar' }, ], data_source => 'TheNamsapce::DataSource::TheDatabase', }; Note that the reverse_as parameter of the container property actually points to the object accessor, not the id accessor. It doesn't make sense, but that's how it is for now. Hopefully we'll come up with a better syntax. =head2 Has-many The contained class/table has a foreign key pointing to the container it's a part of. table Container column type constraint ------------------------------------------ container_id Integer primary key value Varchar not null table Contained column type constraint ------------------------------------------ contained_id Integer primary key contained_value Varchar not null container_id Integer references container(container_id) To create a reverse relationship, you must first create a forward relationship on the related class pointing back to the primary class. Then, creating the reverse relationship involves adding a property where the 'is' is the name of the related class, and a 'reverse_as' indicating which property on the related class describes the forward relationship between that related class and the primary class. class TheNamespace::Container { table_name => 'container', id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, containeds => { is => 'TheNamespace::Contained', reverse_as => 'container', is_many => 1 }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Contained { table_name => 'contained', id_by => [ contained_id => { is => 'Integer' }, ], has => [ contained_value => { is => 'Varchar' }, container_id => { is => 'Integer' }, container => { is => 'TheNamespace::Container', id_by => 'container_id' }, ], data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Many-to-many Storing a has-many relationship requires a bridge table between the two main entities. table Container column type constraint -------------------------------------------- container_id Integer primary key value Varchar not null table Contained column type constraint -------------------------------------------- contained_id Integer primary key contained_value Varchar not null container_id Integer references container(container_id) table Bridge column type constraint -------------------------------------------- container_id Integer references container(container_id) contained_id Integer references contained(contained_id) primary key(container_id,contained_id) Here, both the Container and Contained classes have accessors to return a list of all the objects satisfying the relationship through the bridge table. class TheNamespace::Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ value => { is => 'Varchar' }, ], has_many => [ bridges => { is => 'TheNamespace::Bridge', reverse_as => 'container' }, containeds => { is => 'TheNamespace::Contained', via => 'bridge', to => 'contained' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Bridge { id_by => [ container_id => { is => 'Integer' }, contained_id => { is => 'Integer' }, ], has => [ container => { is => 'TheNamespace::Container', id_by => 'container_id' }, contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, ], table_name => 'bridge', data_source => 'TheNamespace::DataSource::TheDatabase', }; class TheNamespace::Contained { id_by => [ container_id => { is => 'Integer' }, ], has => [ contained_value => { is => 'Varchar' }, ], has_many => [ bridges => { is => 'TheNamespace::Bridge', reverse_as => 'contained' }, containers => { is => 'TheNamespace::Container', via => 'bridge', to => 'container' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head1 Indirect Properties Indirect properties are used to add a property to a class where the data is actually stored in a direct property of a related class. =head2 Singly-indirect As in the has-a relationship, and the container class wants to have a property actually stored on the contained class. Using the same schema in the has-a relationship above, and we want the contained_value property to be accessible from the container class. class TheNamespace::Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ # This implies a contained_id property, too contained => { is => 'TheNamespace::Contained', id_by => 'contained_id' }, contained_value => { via => 'contained', to => 'contained_value' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; You can now use C as an accessor on TheNamespace::Container objects. You can also use C as a parameter in C, and the underlying data source will use a join if possible in the SQL query. =head2 Many Singly-indirect As in the singly-indirect recipe, but the container-contained relationship is has-many class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ containeds => { is => 'TheNamespace::Contained', reverse_as => 'container', is_many => 1 }, contained_values => { via => 'containeds', to => 'container_value', is_many => 1 }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Doubly-indirect If you have a normal has-a relationship between a container and a contained item, and the contained item also has-a third-level contained thing, and you'd like to have a property of the innermost class available to the first container: class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ contained => { is => 'TheNamsepace::Contained', id_by => 'contained_id '}, inner_contained => { is => 'TheNamespace::InnerContained, via => 'contained', to => 'inner_contained_id' }, inner_contained_value => { via => 'inner_contained', to => 'inner_contained_value' }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; =head2 Many doubly-indirect Combining the has-many relationship and the doubly indirect recipe class Container { id_by => [ container_id => { is => 'Integer' }, ], has => [ containeds => { is => 'TheNamsepace::Contained', reverse_as => 'container', is_many => 1}, inner_containeds => { is => 'TheNamespace::InnerContained, via => 'contained', to => 'contained', is_many => 1 }, inner_contained_values => { via => 'inner_containeds', to => 'inner_contained_value', is_many => 1 }, ], table_name => 'container', data_source => 'TheNamespace::DataSource::TheDatabase', }; And then you get an accessor inner_containeds to return a list of inner-contained objects, and another accessor inner_contained_values to return a list of their values. Overview.pod000444023532023421 2365512121654174 16477 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::Overview - UR from Ten Thousand Feet =head1 Perspective on Objects Standard software languages provide a facility for making objects. Those objects have certain characteristics which are different with UR objects. A standard object in most languages: =over 4 =item * exists only as long as the program which created it has a reference to it =item * requires that the developer manage organizing the object(s) into a structure to support any searching required =item * handles persistence between processes explicitly, by saving or loading the object to external storage =item * references other objects only if explicitly linked to those objects =item * acts as a functional software device, but any meaning associated with the object is implied by how it is used =back Regular objects like those described above are the building blocks of most software. In many cases, however, they are often used for a second, higher-level purpose: defining entities in the domain model of the problem area the software addresses. UR objects are tailored to represent domain model entities well. In some sense, UR objects follow many of the design principles present in relational databases, and as such mapping to a database for UR objects is trivial, and can be done in complex ways. UR objects differ from a standard object in the following key ways: =over 4 =item * the object exists after creation until explicitly deleted, or the transaction it is in rolled-back =item * managing loaded objects is done automatically by a Context object, which handles queries, saving, lazy-loading and caching =item * it is possible to query for an object by specifying the class and the matching characteristics =item * the object can reference other objects which are not loaded in the current process, and be referenced by objects not in the current process =item * the object is a particular truth-assertion in the context in which it exists =back =head1 Object-Relational Mapping UR's primary reason for existing is to function as an ORM. That is, managing how to store instances of objects in memory of a running program with more persistent storage in a relational database, and retrieve them later. It handles the common cases where each table is implemented by a class their columns are properties of the classes; retrieving objects by arbitrary properties; creating, updating and deleting objects with enforced database constraints; and named relationships between classes. It can also handle more complicated things like: =over 4 =item * classes for things which are not database entities at all =item * derived classes where the data spans multiple tables between the parent and child classes =item * loading an object through a parent class and having it automatically reblessed into the appropriate subclass =item * properties with no DB column behind them =item * calculated properties with a formula behind them =item * inheritance hierarchies that may have tables missing at some or all stages =item * meta-data about Properties, Classes and the relationships between them =back =head1 Object Context With UR, every object you create is made a part of the current "Context". Conceptually, the Context is the lens by which your application views the data that exists in the world. At one level, you can think of the current context as an in-memory transaction. All changes to the object are tracked by the context. The Context knows how to map objects to their storage locations, called Data Sources. Saving your changes is simply a matter of asking the current context to commit. The Context can also reverse the saving process, and map a request for an object to a query of external storage. Requests for objects go through the Context, are loaded from outside as needed, and are returned to the caller after being made part of the current context's transaction. Objects never reference each other by actual Perl reference internally, instead they use the referent's ID. Accessors on an object which return another object send the ID through the context to get the object back, allowing the context to load the referenced object only when it is actually needed. This means that your objects can hook together until references span an entire database schema, and pulling one object from the database will not load the entire database into memory. The context handles caching, and by default will cache everything it touches. This means that you can ask for the same thing multiple times, and only the first request will actually hit the underlying database. It also means that requests for objects which map to the same ID will return the exact same instance of the object. The net effect is that each process's context is an in-memory database. All object creation, deletion, and change is occurring directly to that database. For objects configured to have external persistence, this database manages itself as a "diff" vs. the external database, allowing it to simulate representing all UR data everywhere, while only actually tracking what is needed. =head2 Benefits =over 4 =item * database queries don't repeat themselves again and again =item * you never write insert/update/delete statements, or work out constraint order yourself =item * allows you to write methods which address an object individually, with ways to avoid many individual database queries =item * explicitly clearing the cache is less complex than explicitly managing the caching of data =back =head2 Issues =over 4 =item * the cache grows until you explicitly clear it, or allow the Context to prune the cache by setting object count limits explicitly =item * there is CPU overhead checking the cache if you really are always going directly to the database =back =head1 Class Definitions At the top of every module implementing a UR class is a block of code that defines the class to explicitly spell out its inheritance, properties and types, constraints, relationships to other classes and where the persistent storage is located. It's meant to be easy to read and edit, if necessary. If the class is backed by a database table, then it can also maintain itself. =head1 Metadata Besides the object instances representing data used by the program, the UR system has other objects representing metadata about the classes (class information, properties, relationships, etc), database entities (databases, tables, columns, constraints, etc), transactions, data sources, etc. All the metadata is accessible through the same API as any of the database-backed data. For classes backed by the database, after a schema change (like adding tables or columns, altering types or constraints), a command-line tool can automatically detect the change and alter the class definition in the Perl module to keep the metadata in sync with the database. =head1 Documentation System At the simplest level, most entities have a 'doc' metadata attribute to attach some kind of documentation to. There's also a set of tools that can be run from the command line or a web browser to view the documentation. It can also be used to browse through the class and database metadata, and generate diagrams about the metadata. =head1 Iterators If a retrieval from the database is likely to result in the generation of tons of objects, you can choose to get them back in a list and keep them all in memory, or get back a special Iterator object that the program can use to get back objects in batches. =head1 Command Line Tools UR has a central command-line tool that cam be used to manipulate the metadata in different ways. Setting up namespaces, creating data sources, syncing classes with schemas, accessing documentation, etc. There is also a framework for creating classes that represent command line tools, their parameters and results, and makes it easy to create tools through the Command Pattern. =head1 Example Given these classes: =over 4 =item PathThing/Path.pm use strict; use warnings; use PathThing; # The application's UR::Namespace module class PathThing::Path { id_by => 'path_id', has => [ desc => { is => 'String' }, length => { is => 'Integer' }, ], data_source => 'PathThing::DataSource::TheDB', table_name => 'PATHS', }; =item PathThing/Node.pm class PathThing::Node { id_by => 'node_id', has => [ left_path => { is => 'PathThing::Path', id_by => 'left_path_id' }, left_path_desc => { via => 'left_path', to => 'desc' }, left_path_length => { via => 'left_path', to => 'length' }, right_path => { is => 'PathThing::Path', id_by => 'right_path_id' }, right_path_desc => { via => 'right_path', to => 'desc' }, right_path_length => { via => 'right_path', to => 'length' }, map_coord_x => { is => 'Integer' }, map_coord_y => { is => 'String' }, ], data_source => 'PathThing::DataSource::TheDB', table_name => 'NODES', }; =back For a script like this one: use PathThing::Node; my @results = PathThing::Node->get( right_path_desc => 'over the river', left_path_desc => 'through the woods', right_path_length => 10, ); It will generate SQL like this: select NODES.NODE_ID, NODES.LEFT_PATH_ID, NODES.RIGHT_PATH_ID, NODES.MAP_COORD_X, NODES.MAP_COORD_Y, left_path_1.PATH_ID, left_path_1.DESC, left_path_1.LENGTH right_path_1.PATH_ID, right_path_1.DESC, right_path_1.LENGTH from NODES join PATHS left_path_1 on NODES.LEFT_PATH_ID = left_path_1.PATH_ID join PATHS right_path_1 on NODES.RIGHT_PATH_ID = right_path1.PATH_ID where left_path_1.DESC = 'through the woods' and right_path_1.DESC = 'over the river', and right_path_1.LENGTH = 10 And for every row returned by the query, a PathThing::Node and two PathThing::Path objects will be instantiated and stored in the Context's cache. C<@results> will contain a list of matching PathThing::Node objects. Tutorial.pod000444023532023421 3305612121654174 16470 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::Tutorial - Step-by-step guide to building a set of classes for a simple database schema =head1 Overview We'll use the familiar "Music Database" example used in many ORM tutorials: Our database has the following basic entities and relationships: =over 2 =item * One artist can have many CDs =item * One CD belongs to one artist =item * one CD can have many tracks =item * one track belongs to one CD =back =head1 The "ur" command-line program The tool for working with UR from the command line is 'ur' . It is installed with the UR module suite. Just type "ur" and hit enter, to see a list of valid ur commands: > ur Sub-commands for ur: init NAMESPACE [DB] initialize a new UR app in one command define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging The "ur" command works a lot like the "svn" command: it is the entry point for a list of other subordinate commands. =over 2 =item * Typing something like "ur browser" will run the browser tool. =item * Typing something like "ur define" will give another list, of even more granular commands which are under "ur define": =back > ur define Sub-commands for ur define: namespace NSNAME create a new namespace tree and top-level module db URI NAME add a data source to the current namespace class --extends=? [NAMES] Add one or more classes to the current namespace At any point, you can put '--help' as a command line argument and get some (hopefully) helpful documentation. In many cases, the output also resembles svn's output where the first column is a character like 'A' to represent something being added, 'D' for deleted, etc. (NOTE: The "ur" command, uses the Command API, an API for objects which follow the command-pattern. See L for more details on writing tools like this. =head1 Define a UR Namespace A UR namespace is the top-level object that represents your data's class structure in the most general way. For this new project, we'll need to create a new namespace, perhaps within a testing directory. ur define namespace Music And you should see output like this: A Music (UR::Namespace) A Music::Vocabulary (UR::Vocabulary) A Music::DataSource::Meta (UR::DataSource::Meta) A Music/DataSource/Meta.sqlite3-dump (Metadata DB skeleton) showing that it created 3 classes for you, Music, Music::Vocabulary and Music::DataSource::Meta, and shows what classes those inherit from. In addition, it has also created a file to hold your metadata. Other parts of the documentation give a more thorough description of Vocabulary and Metadata classes. =head1 Define a Data Source A UR DataSource is an object representing the location of your data. It's roughly analogous to a Schema class in DBIx::Class, or the "Base class" in Class::DBI. Note: Because UR can be used with objects which do NOT live in a database, using a data source is optional, but is the most common case. Most ur commands operate in the context of a Namespace, including the one to create a datasource, so you need to be within the Music's Namespace's directory: cd Music and then define the datasource. We specify the data source's type as a sub-command, and the name with the --dsname argument. For this example, we'll use a brand new SQLite database. For some other, perhaps already existing database, give its connect string instead. ur define db dbi:SQLite:/var/lib/music.sqlite3 Example which generates this output: A Music::DataSource::Example (UR::DataSource::SQLite,UR::Singleton) ...connecting... ....ok and creates a symlink to the database at: Music/DataSource/Example.sqlite3 and shows that it created a class for your data source called Music::DataSource::Example, which inherits from UR::DataSource::SQLite. It also created an empty database file and connected to it to confirm that everything is OK. =head1 Create the database tables Here are the table creation statements for our example database. Put them into a file with your favorite editor and call it example-db.schema.txt: CREATE TABLE artist ( artist_id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE cd ( cd_id INTEGER NOT NULL PRIMARY KEY, artist_id INTEGER NOT NULL CONSTRAINT CD_ARTIST_FK REFERENCES artist(artist_id), title TEXT NOT NULL, year INTEGER ); CREATE TABLE track ( track_id INTEGER NOT NULL PRIMARY KEY, cd_id INTEGER NOT NULL CONSTRAINT TRACK_CD_FK REFERENCES cd(cd_id), title TEXT NOT NULL ); This new SQLite data source assumes the database file will have the pathname Music/DataSource/Example.sqlite3. You can populate the database schema like this: sqlite3 DataSource/Example.sqlite3 < example-db.schema.txt =head1 Create your data classes Now we're ready to create the classes that will store your data in the database. You could write those classes by hand, but it's easiest to start with an autogenerated group built from the database schema: ur update classes-from-db is the command that performs all the magic. You'll see it go through several steps: =over 2 =item 1. Find all the defined datasources within the current namespace =item 2. Query the data sources about what tables, columns, constraints and foreign keys are present =item 3. Load up all the classes in the current namespace =item 4. Figure out what the differences are between the database schema and the class structure =item 5. Alter the class metadata to match the database schema =item 6. Use the new class metadata to write headers on the Perl module files in the namespace =back There will now be a Perl module for each database table. For example, in Cd.pm: package Music::Cd; use strict; use warnings; use Music; class Music::Cd { table_name => 'CD', id_by => [ cd_id => { is => 'INTEGER' }, ], has => [ artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' }, artist_id => { is => 'INTEGER' }, title => { is => 'TEXT' }, year => { is => 'INTEGER', is_optional => 1 }, ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; 1; The first few lines are what you would see in any Perl module. The keyword C tells the UR system to define a new class, and lists the properties of the new class. Some of the important parts are that instances of this class come from the Music::DataSource::Example datasource, in the table 'CD'. This class has 4 direct properties (cd_id, artist_id, title and year), and one indirect property (artist). Instances are identified by the cd_id property. Methods are automatically created to match the property names. If you have an instance of a CD, say $cd, you can get the value of the title with C<$cd-Etitle>. To get back the artist object that is related to that CD, C<$cd-Eartist>. =head1 CRUD (Create, Read, Update, Delete) =head2 Create Creating new object instances is done with the create method; its arguments are key-value pairs of properties and their values. #!/usr/bin/perl use strict; use Music; my $obj1 = Music::Artist->create(name => 'Elvis'); my $obj2 = Music::Artist->create(name => 'The Beatles'); UR::Context->commit(); And that's it. After this script runs, there will be 2 rows in the Artist table. Just a short aside about that last line... All the changes to your objects while the program runs (creates, updates, deletes) exist only in memory. The current "Context" manages that knowledge. Those changes are finally pushed out to the underlying data sources with that last line. =head2 Read Retrieving object instances from the database is done with the C method. A C with no arguments will return a list of all the objects in the table. @all_cds = Music::Cd->get(); If you know the "id" (primary key) value of the objects you're interested in, you can pass that "id" value as a single argument to get: $cd = Music::Cd->get(3); An arrayref of identity values can be passed-in as well. Note that if you query is going to return more than one item, and it is called in scalar context, it will generate an exception. @some_cds = Music::Cd->get([1, 2, 4]); To filter the return list by a property other than the ID property, give a list of key-value pairs: @some_cds = Music::Cd->get(artist_id => 3); This will return all the CDs with the artist ID 5, 6 or 10. @some_cds = Music::Cd->get(artist_id => [5, 6, 10]); get() filters support operators other than strict equality. This will return a list of CDs with artist ID 2 and have the word 'Ticket' somewhere in the title. @some_cds = Music::Cd->get(artist_id=> 2, title => { operator => 'like', value => '%Ticket%'} ); To search for NULL fields, use undef as the value: @cds_with_no_year = Music::Cd->get(year => undef); =head2 get_or_create C is used to retrieve an instance from the database if it exists, or create a new one if it does not. $possibly_new = Music::Artist->get_or_create(name => 'The Band'); =head2 Update All the properties of an object are also mutators. To change the object's property, just call the method for that property with the new value. $cd->year(1990); Remember that any changes made while the program runs are not saved in the database until you commit the changes with Ccommit>. =head2 Delete The C method does just what it says. @all_tracks = Music::Track->get(); foreach my $track ( @all_tracks ) { $track->delete(); } Again, the corresponding database rows will not be removed until you commit. =head1 Relationships After running ur update classes, it will automatically create indirect properties for all the foreign keys defined in the schema, but not for the reverse relationships. You can add other relationships in yourself and they will persist even after you run ur update classes again. For example, there is a foreign key that forces a track to be related to one CD. If you edit the file Cd.pm, you can define a relationship so that CDs can have many tracks: class Music::Cd { table_name => 'CD', id_by => [ cd_id => { is => 'INTEGER' }, ], has => [ artist => { is => 'Music::Artist', id_by => 'artist_id', constraint_name => 'CD_ARTIST_FK' }, artist_id => { is => 'INTEGER' }, title => { is => 'TEXT' }, year => { is => 'INTEGER' }, tracks => { is => 'Music::Track', reverse_as => 'cd', is_many => 1 }, # This is the new line ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; This tells the system that there is a new property called 'tracks' which returns items of the class Music::Track. It links them to the acting CD object through the Track's cd property. After that is in place, you can ask for a list of all the tracks belonging to a CD with the line @tracks = $cd->tracks() You can also define indirect relationships through other indirect relationships. For example, if you edit Artist.pm to add a couple of lines: class Music::Artist { table_name => 'ARTIST', id_by => [ artist_id => { is => 'INTEGER' }, ], has => [ name => { is => 'TEXT' }, cds => { is => 'Music::Cd', reverse_as => 'artist', is_many => 1 }, tracks => { is => 'Music::Track', via => 'cds', to => 'tracks', is_many => 1}, ], schema_name => 'Example', data_source => 'Music::DataSource::Example', }; This defines a relationship 'cds' to return all the CDs from the acting artist. It also defines a relationship called 'tracks' that will, behind the scenes, first look up all the CDs from the acting artist, and then find and return all the tracks from those CDs. Additional arguments can be passed to these indirect accessors to get a subset of the data @cds_in_1990s = $artist->cds(year => { operator => 'between', value => [1990,1999] } ); would get all the CDs from that artist where the year is between 1990 and 1999, inclusive. Note that is_many relationships should always be named with plural words. The system will auto-create other accessors based on the singular name for adding and removing items in the relationship. For example: $artist->add_cd(year => 1998, title => 'Cool Jams' ); would create a new Music::Cd object with the given year and title. The cd_id will be autogenerated by the system, and the artist_id will be automatically set to the artist_id of $artist. =head1 Custom SQL It's possible to use get() with custom SQL to retrieve objects, as long as the select clause includes all the ID properties of the class. To find Artist objects that have no CDs, you might do this: my @artists_with_no_cds = Music::Artist->get(sql => 'select artist.artist_id, count(cd.artist_id) from artist left join cd on cd.artist_id = artist.artist_id group by artist.artist_id having count(cd.artist_id) = 0' ); Presentation.pod000444023532023421 67612121654175 17303 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Manual=pod =head1 NAME UR::Manual::Presentation - Slides for a presenation on UR =head1 Overview UR_Presentation.pdf is located in the Manual/ subdirectory of the UR distribution. It contains slides for a presentation on UR originally given at the Lambda Lounge meeting in St. Louis, MO on May 6 2009. The presentation covers the implementation of an over-the-top vending machine that stores its contents and inventory in an SQLite database. Context000755023532023421 012121654175 14205 5ustar00abrummetgsc000000000000UR-0.41/lib/URProcess.pm000444023532023421 3024612121654173 16341 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::Process; =pod =head1 NAME UR::Context::Process - Impliments a generic interface to the current application. =head1 SYNOPSIS $name = UR::Context::Process->base_name; $name = UR::Context::Process->prog_name; UR::Context::Process->prog_name($name); $name = UR::Context::Process->pkg_name; UR::Context::Process->pkg_name($name); $name = UR::Context::Process->title; UR::Context::Process->title($name); $version = UR::Context::Process->version; UR::Context::Process->version($version); $author = UR::Context::Process->author; UR::Context::Process->author($author); $author_email = UR::Context::Process->author_email; UR::Context::Process->author_email($author_email); $support_email = UR::Context::Process->support_email; UR::Context::Process->support_email($support_email); $login = UR::Context::Process->real_user_name; =head1 DESCRIPTION This module provides methods to set and retreive variaous names associated with the program and the program version number. =cut package UR::Context::Process; our $VERSION = "0.41"; # UR $VERSION;; require 5.006_000; use strict; use warnings; use Sys::Hostname; use File::Basename; require UR; UR::Object::Type->define( class_name => 'UR::Context::Process', is => ['UR::Context'], is_transactional => 0, has => [ host_name => { is => 'Text' }, process_id => { is => 'Integer' }, access_level => { is => 'Text', default_value => '??' }, debug_level => { is => 'Integer', default_value => 0 }, ], doc => 'A context for a given process.', ); =pod =head1 METHODS These methods provide the accessor and set methods for various names associated with an application. =over =item get_current $ctx = UR::Context::Process->get_current(); This is the context which represents the current process. Also available as UR::Context->get_process(); =back =cut sub get_current { return $UR::Context::process; } =pod =over =item has_changes() $bool = UR::Context::Process->has_changes(); Returns true if the current process has changes which might be committed back to the underlying context. =back =cut sub has_changes { my $self = shift; my @ns = $self->all_objects_loaded('UR::Namespace'); for my $ns (@ns) { my @ds = $ns->get_data_sources(); for my $ds (@ds) { return 1 if $ds->has_changes_in_base_context(); } } return; } =pod =over =item _create_for_current_process $ctx = UR::Context::Process->_create_for_current_process(@PARAMS) This is only used internally by UR. It materializes a new object to represent a real process somewhere. TODO: Remove the exception from create(), and allow other processes to be created explicitly w/ the appropriate characteristics. =back =cut sub _create_for_current_process { my $class = shift; die "Process object for the current process already exists!" if $UR::Context::process; #my $rule = $class->define_boolexpr(@_); my $rule = UR::BoolExpr->resolve($class, @_); my $host_name = Sys::Hostname::hostname(); my $id = $host_name . "\t" . $$; my $self = $class->SUPER::create(id => $id, process_id => $$, host_name => $host_name, $rule->params_list); return $self; } sub create { # Note that the above method does creation by going straight to SUPER::create() # for the current process only. die "Creation of parallel/child processes not yet supported!" } # TODO: the remaining methods are from the old App::Name module. # They currently only work for the current process, and operate as class methods. # They should be re-written to work as class methods on $this_process, or # instance methods on any process. For now, only the class methods are needed. =pod =over =item base_name $name = UR::Context::Process->base_name; This is C. =back =cut our $base_name = basename($0, '.pl'); sub base_name { return $base_name } =pod =over =item prog_name $name = UR::Context::Process->prog_name; UR::Context::Process->prog_name($name); This method is used to access and set the name of the program name. This name is used in the output of the C and C subroutines (see L<"version"> and L<"usage">). If given an argument, this method sets the program name and returns the new name or C if unsuccessful. It defaults to C if unspecified. =back =cut our $prog_name; sub prog_name { my $class = shift; my ($name) = @_; if (@_) { $prog_name = $name; } return $prog_name || $class->base_name; } =pod =over =item pkg_name $name = UR::Context::Process->pkg_name; UR::Context::Process->pkg_name($name); This method is used to access and set the GNU-standard package name for the package to which this program belongs. This is does B refer-to a Perl package. It allows a set of spefic programs to be grouped together under a common name, which is used in standard message output, and is used in the output of the C subroutine (see L<"version"> output. If given an argument, this method sets the package name and returns the the new name or C if unsuccessful. Without an argument, the current package name is returned. It defaults to C when unspecified, which in turn defaults to C, which in turn defaults to C. =back =cut # NOTE: this should not use App::Debug because App::Debug::level calls it our $pkg_name; sub pkg_name { my $class = shift; my ($name) = @_; if (@_) { $pkg_name = $name; } return $pkg_name || $class->prog_name; } =pod =over =item title $name = UR::Context::Process->title; UR::Context::Process->title($name); This gets and sets the "friendly name" for an application. It is often mixed-case, with spaces, and is used in autogenerated documentation, and sometimes as a header in generic GUI components. Without an argument, it returns the current title. If an argument is specified, this method sets the application title and returns the new title or C if unsuccessful. It defaults to C when otherwise unspecified, which in turn defaults to C when unspecified, which in turn defaults to C when unspecified, which defaults to C when unspecified. =back =cut our $title; sub title { my $class = shift; my ($name) = @_; if (@_) { $title = $name; } return $title || $class->pkg_name; } =pod =over =item version $version = UR::Context::Process->version; UR::Context::Process->version($version); This method is used to access and set the package version. This version is used in the output of the C method (see L). If given an argument, this method sets the package version and returns the version or C if unsuccessful. Without an argument, the current package version is returned. This message defaults to C<$main::VERSION> if not set. Note that C<$main::VERSION> may be C. =back =cut # set/get version # use $main::VERSION for compatibility with non-App animals. sub version { my $class = shift; my ($version) = @_; if (@_) { $main::VERSION = $version; } return $main::VERSION; } =pod =over =item author $author = UR::Context::Process->author; UR::Context::Process->author($author); This method is used to access and set the package author. If given an argument, this method sets the package author and returns the author or C if unsuccessful. Without an argument, the current author is returned. =back =cut # set/get author our $author; sub author { my $class = shift; my ($name) = @_; if (@_) { $author = $name; } return $author; } =pod =over =item author_email $author_email = UR::Context::Process->author_email; UR::Context::Process->author_email($author_email); This method is used to access and set the package author's email address. This information is used in the output of the C method (see L). If given an argument, this method sets the package author's email address and returns email address or C if unsuccessful. Without an argument, the current email address is returned. =back =cut # set/return author email address our $author_email; sub author_email { my $class = shift; my ($email) = @_; if (@_) { $author_email = $email; } return $author_email; } =pod =over =item support_email $support_email = UR::Context::Process->support_email; UR::Context::Process->support_email($support_email); This method is used to access and set the email address to which the user should go for support. This information is used in the output of the C method (see L). If given an argument, this method sets the support email address and returns that email address or C if unsuccessful. Without an argument, the current email address is returned. =back =cut # set/return author email address our $support_email; sub support_email { my $class = shift; my ($email) = @_; if (@_) { $support_email = $email; } return $support_email; } =pod =over =item real_user_name $login = UR::Context::Process->real_user_name; This method is used to get the login name of the effective user id of the running script. =back =cut # return the name of the user running the program our $real_user_name; sub real_user_name { my $class = shift; if (!$real_user_name) { if ($^O eq 'MSWin32' || $^O eq 'cygwin') { $real_user_name = 'WindowsUser'; } else { $real_user_name = getpwuid($<) || getlogin || 'unknown'; } } return $real_user_name; } =pod =over =item fork $pid = UR::Context::Process->fork; Safe fork() wrapper. Handles properly disconnecting database handles if necessary so that data sources in children are still valid. Also ensures that the active UR::Context::process has the child's PID recorded within. =back =cut sub fork { my $class = shift; my @ds = UR::DataSource->is_loaded(); for (grep {defined $_} @ds) { $_->prepare_for_fork; } my $pid = fork(); unless(defined $pid) { Carp::confess('Failed to fork process. ' . $!); } if (!$pid) { $UR::Context::process = undef; $UR::Context::process = $class->_create_for_current_process; for (grep {defined $_} @ds) { $_->do_after_fork_in_child; } } for (grep {defined $_} @ds) { $_->finish_up_after_fork; } return $pid; } =pod =over =item effective_user_name $login = UR::Context::Process->effective_user_name; This method is used to get the login name of the effective user id of the running script. =back =cut # return the name of the user running the program our $effective_user_name; sub effective_user_name { my $class = shift; if (!$effective_user_name) { $effective_user_name = getpwuid($>) || 'unknown'; } return $effective_user_name; } =pod =over =item original_program_path $path = UR::Context::Process->original_program_path; This method is used to (try to) get the original program path of the running script. This will not change even if the current working directory is changed. (In truth it will find the path at the time UR::Context::Process was used. So, a chdir before that happens will cause incorrect results; in that case, undef will be returned. =back =cut our ($original_program_name, $original_program_dir); eval ' use FindBin; $original_program_dir=$FindBin::Bin; $original_program_name=__PACKAGE__->base_name; '; sub original_program_path { my $class=shift; my $original_program_dir=$class->original_program_dir; return unless($original_program_dir); my $original_program_name=$class->original_program_name; return unless($original_program_name); return $original_program_dir.q(/).$original_program_name; } sub original_program_dir { return unless($original_program_dir); return $original_program_dir; } sub original_program_name { return unless($original_program_name); return $original_program_name; } 1; __END__ =pod =head1 SEE ALSO L =cut 1; LoadingIterator.pm000444023532023421 7201512121654173 20012 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::LoadingIterator; use strict; use warnings; use UR::Context; our $VERSION = "0.41"; # UR $VERSION; # A helper package for UR::Context to handling queries which require loading # data from outside the current context. It is responsible for collating # cached objects and incoming objects. When create_iterator() is used in # application code, this is the iterator that gets returned # # These are normal Perl objects, not UR objects, so they get regular # refcounting and scoping our @CARP_NOT = qw( UR::Context ); # A boolean flag used in the loading iterator to control whether we need to # inject loaded objects into other loading iterators' cached lists my $is_multiple_loading_iterators = 0; my %all_loading_iterators; # The set of objects returned by an iterator is initially determined when the # iterator is created, but the final determination of membership happens when # the object is about to be returned from the iterator's next() method. # In practice, this means that an object matches the BoolExpr at iterator # creation, and no longer matches when that object is about to be returned, # it will not be returned. # # If an object does not match the bx when the iterator is created, it will # not be returned even if it later changes to match before the iterator is # exhausted. # # If an object changes so that it's sort order changes after the iterator is # created but before it is returned by the iterator, the object will be # returned in the order it had at iterator creation time. # Finally, the LoadingIterator will throw an exception if an object matches # the BoolExpr at iterator creation time, but is deleted when next() is about # to return it (ie. isa UR::DeletedRef). Since DeletedRef's die any time you # try to use them, the object sorters can't sort them. Instead, we'll just # punt and throw an exception ourselves if we come across one. # # This seems like the least suprising thing to do, but there are other solutions: # 1) just plain don't return the deleted object # 2) use signal_change to register a callback which will remove objects being deleted # from all the in-process iterator @$cached lists (accomplishes the same as #1). # For completeness, this may imply that other signal_change callbacks would remove # objects that no longer match rules for in-process iterators, and that means that # next() returns things true at the time next() is called, not when the iterator # is created. # 3) Put in some additional infrastructure so we can pull out the ID of a deleted # object. That lets us call $next_object->id at the end of the closure, and return these # deleted objects back to the user. Problem being that the user then can't really # do anything with them. But it would be consistent about returning _all_ objects # that matched the rule at iterator creation time # 4) Like #3, but just always return the deleted object before any underlying_context # object, and then don't try to get its ID at the end if the iterator if it's deleted sub _create { my($class, $cached, $context, $normalized_rule, $data_source, $this_get_serial ) = @_; my $underlying_context_iterator = $context->_create_import_iterator_for_underlying_context( $normalized_rule, $data_source, $this_get_serial); my $is_monitor_query = $context->monitor_query; # These are captured by the closure... my($last_loaded_id, $next_obj_current_context, $next_obj_underlying_context); my $object_sorter = $normalized_rule->template->sorter(); my $bx_subject_class = $normalized_rule->subject_class_name; # Collection of object IDs that were read from the DB query. These objects are for-sure # not deleted, even though a cached object for it might have been turned into a ghost or # had its properties changed my %db_seen_ids_that_are_not_deleted; # Collection of object IDs that were read from the cached object list and haven't been # seen in the lsit of results from the database (yet). It could be missing from the DB # results because that row has been deleted, because the DB row still exists but has been # changed since we loaded it and now doesn't match the BoolExp, or because we're sorting # results by something other than just ID, that sorted property has been changed in the DB # and we haven't come across this row yet but will before. # # The short story is that if there is anything in this hash when the underlying context iterator # is exhausted, then the ID-ed object is really deleted, and should be an exception my %changed_objects_that_might_be_db_deleted; my $underlying_context_objects_count = 0; my $cached_objects_count = 0; # knowing if an object's changed properties are one of the rule's order-by # properties helps later on in the loading process of detecting deleted DB rows my %order_by_properties; if ($normalized_rule->template->order_by) { %order_by_properties = map { $_ => 1 } @{ $normalized_rule->template->order_by }; } my $change_is_order_by_property = sub { foreach my $prop_name ( shift->_changed_property_names ) { return 1 if exists($order_by_properties{$prop_name}); } return; }; my %bx_filter_properties = map { $_ => 1 } $normalized_rule->template->_property_names; my $change_is_bx_filter_property = sub { foreach my $prop_name ( shift->_changed_property_names ) { return 1 if exists($bx_filter_properties{$prop_name}); } return; }; my $limit = $normalized_rule->template->limit; my $offset = $normalized_rule->template->offset; my $me_loading_iterator_as_string; # See note below the closure definition my $loading_iterator = sub { return if (defined($limit) and !$limit); my $next_object; PICK_NEXT_OBJECT_FOR_LOADING: while (! defined($next_object)) { if ($underlying_context_iterator && ! defined($next_obj_underlying_context)) { ($next_obj_underlying_context) = $underlying_context_iterator->(1); $underlying_context_objects_count++ if ($is_monitor_query and defined($next_obj_underlying_context)); if (defined($next_obj_underlying_context)) { if ($next_obj_underlying_context->isa('UR::DeletedRef')) { # This object is deleted in the current context and not yet committed # skip it and pick again $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($next_obj_underlying_context->__changes__ and $change_is_order_by_property->($next_obj_underlying_context) ) { unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context->id}) { $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context->id} = 1; } $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } } unless (defined $next_obj_current_context) { ($next_obj_current_context) = shift @$cached; $cached_objects_count++ if ($is_monitor_query and $next_obj_current_context); } if (defined($next_obj_current_context) and $next_obj_current_context->isa('UR::DeletedRef')) { my $obj_to_complain_about = $next_obj_current_context; # undef it in case the user traps the exception, next time we'll pull another off the list $next_obj_current_context = undef; Carp::croak("Attempt to fetch an object which matched $normalized_rule when the iterator was created, " . "but was deleted in the meantime:\n" . Data::Dumper::Dumper($obj_to_complain_about) ); } if (!defined($next_obj_underlying_context)) { if ($is_monitor_query) { $context->_log_query_for_rule($bx_subject_class, $normalized_rule, "QUERY: loaded $underlying_context_objects_count object(s) total from underlying context."); } $underlying_context_iterator = undef; # Anything left in this hash when the DB iterator is exhausted are object we expected to # see by now and must be deleted. If any of these object have changes then # the __merge below will throw an exception foreach my $problem_obj (values(%changed_objects_that_might_be_db_deleted)) { $context->__merge_db_data_with_existing_object($bx_subject_class, $problem_obj, undef, []); } } elsif (defined($last_loaded_id) and $last_loaded_id eq $next_obj_underlying_context->id) { # during a get() with -hints or is_many+is_optional (ie. something with an # outer join), it's possible that the join can produce the same main object # as it's chewing through the (possibly) multiple objects joined to it. # Since the objects will be returned sorted by their IDs, we only have to # remember the last one we saw # FIXME - is this still true now that the underlying context iterator and/or # object fabricator hold off on returning any objects until all the related # joined data bas been loaded? $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } # decide which pending object to return next # both the cached list and the list from the database are sorted separately but with # equivalent algorithms (we hope). # # we're collating these into one return stream here my $comparison_result = undef; if (defined($next_obj_underlying_context) && defined($next_obj_current_context)) { $comparison_result = $object_sorter->($next_obj_underlying_context, $next_obj_current_context); } my $next_obj_underlying_context_id; $next_obj_underlying_context_id = $next_obj_underlying_context->id if (defined $next_obj_underlying_context); my $next_obj_current_context_id; $next_obj_current_context_id = $next_obj_current_context->id if (defined $next_obj_current_context); # This if() section is for when the in-memory and DB iterators return the same # object at the same time. if ( defined($next_obj_underlying_context) and defined($next_obj_current_context) and $comparison_result == 0 # $next_obj_underlying_context->id eq $next_obj_current_context->id ) { # Both objects sort the same. Since the ID properties are always last in the sort order list, # this means both objects must be the same object. $context->_log_query_for_rule($bx_subject_class, $normalized_rule, "QUERY: loaded object was already cached") if ($is_monitor_query); $next_object = $next_obj_current_context; $next_obj_current_context = undef; $next_obj_underlying_context = undef; } # This if() section is for when the DB iterator's object sorts first elsif ( defined($next_obj_underlying_context) and ( (!defined($next_obj_current_context)) or ($comparison_result < 0) # ($next_obj_underlying_context->id le $next_obj_current_context->id) ) ) { # db object sorts first # If we deleted it from memorym the DB would not have given it back. # So it either failed to match the BX now, or one of the order-by parameters changed if ($next_obj_underlying_context->__changes__) { # See if one of the changes is an order-by property if ($change_is_order_by_property->($next_obj_underlying_context)) { # If the object has changes, and one of the changes is one of the # order-by properties, then the object will: # 1) Already have appeared as $next_obj_current_context. # it will be in $changed_objects_that_might_be_db_deleted - remove it from that list # 2) Will appear later as $next_obj_current_context. # Mark here that it's not deleted unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context_id}) { $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context_id} = 1; } } elsif ($change_is_bx_filter_property->($next_obj_underlying_context)) { # If the object has any changes, then it will appear in the cached object list in # $next_object_current_context at the appropriate time. For the case where the # object no longer matches the BoolExpr, then the appropriate time is never. # Discard this object from the DB and pick again $next_obj_underlying_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # some other kind of change? $next_object = $next_obj_underlying_context; $next_obj_underlying_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } else { # If the object has no changes, it must be something newly brought into the system. $next_object = $next_obj_underlying_context; $next_obj_underlying_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } # This if() section is for when the in-memory iterator's object sorts first elsif ( defined($next_obj_current_context) and ( (!defined($next_obj_underlying_context)) or ($comparison_result > 0) # ($next_obj_underlying_context->id ge $next_obj_current_context->id) ) ) { # The cached object sorts first # Either it was changed in memory, in the DB or both # In addition, the change could have been to an order-by property, one of the # properties in the BoolExpr, or both if (! $next_obj_current_context->isa('UR::Object::Set') # Sets aren't really from the underlying context and $context->object_exists_in_underlying_context($next_obj_current_context) ) { if ($next_obj_current_context->__changes__) { if ($change_is_order_by_property->($next_obj_current_context)) { # This object is expected to exist in the underlying context, has changes, and at # least one of those changes is to an order-by property # # if it's in %db_seen_ids_that_are_not_deleted, then it was seen earlier # from the DB, and can now be removed from that hash. unless (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) { # If not in that list, then add it to the list of things we might see later # in the DB iterator. If we don't see it by the end if the iterator, it # must have been deleted from the DB. At that time, we'll throw an exception. # It's later than we'd like, since the caller has already gotten ahold of the # object, but better late than never. The alternative is to do an id-only # query right now, but that would be inefficient. # # We could avoid storing this if we could verify that the db_committed/db_saved_uncommitted # values did NOT match the BoolExpr, but this will suffice for now. $changed_objects_that_might_be_db_deleted{$next_obj_current_context_id} = $next_obj_current_context; } # In any case, return the cached object. $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($change_is_bx_filter_property->($next_obj_current_context)) { # The change was that the object originally did not the filter, but since being # loaded it's been changed so it now matches the filter. The DB iterator isn't # returning the object since the DB's copy doesn't match the filter. delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}; $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } else { # The change is not an order-by property. This object must have been deleted # from the DB. The call to __merge below will throw an exception $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []); $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } else { # This cached object has no changes, so the database must have changed. # It could be deleted, no longer match the BoolExpr, or have changes in an order-by property if (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) { # We saw this already on the DB iterator. It's not deleted. Go ahead and return it $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } elsif ($normalized_rule->is_id_only) { # If the query is id-only, and we didn't see the DB object at the same time, then # the DB row must have been deleted. Changing the PK columns in the DB are logically # the same as deleting the old object and creating/defineing a new one in UR. # # The __merge will delete the cached object, then pick again $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []); $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # Force an ID-only query to the underying context my $requery_obj = $context->reload($bx_subject_class, id => $next_obj_current_context_id); if ($requery_obj) { # In any case, the DB iterator will pull it up at the appropriate time, # and since the object has no changes, it will be returned to the caller then. # Discard this in-memory object and pick again $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } else { # We've now confirmed that the object in the DB is really gone # NOTE: the reload() has already performed the __merge (implying deletion) # in the above branch "elsif ($normalized_rule->is_id_only)" so we don't need # to __merge/delete it here $next_obj_current_context = undef; redo PICK_NEXT_OBJECT_FOR_LOADING; } } } } else { # The object does not exist in the underlying context. It must be # a newly created object. $next_object = $next_obj_current_context; $next_obj_current_context = undef; next PICK_NEXT_OBJECT_FOR_LOADING; } } elsif (!defined($next_obj_current_context) and !defined($next_obj_underlying_context) ) { # Both iterators are exhausted. Bail out $next_object = undef; $last_loaded_id = undef; last PICK_NEXT_OBJECT_FOR_LOADING; } else { # Couldn't decide which to pick next? Something has gone horribly wrong. # We're using other vars to hold the objects and setting # $next_obj_current_context/$next_obj_underlying_context to undef so if # the caller is trapping exceptions, this iterator will pick new objects next time my $current_problem_obj = $next_obj_current_context; my $underlying_problem_obj = $next_obj_underlying_context; $next_obj_current_context = undef; $next_obj_underlying_context = undef; $next_object = undef; Carp::croak("Loading iterator internal error. Could not pick a next object for loading.\n" . "Next object underlying context: " . Data::Dumper::Dumper($underlying_problem_obj) . "\nNext object current context: ". Data::Dumper::Dumper($current_problem_obj)); } return unless defined $next_object; # end while ! $next_object } continue { if (defined($next_object) and defined($offset) and $offset) { $offset--; $next_object = undef; } } $last_loaded_id = $next_object->id if (defined $next_object); $limit-- if defined $limit; return $next_object; }; # end of the closure bless $loading_iterator, $class; Sub::Name::subname($class . '__loading_iterator_closure__', $loading_iterator); # Inside the closure, it needs to know its own address, but without holding a real reference # to itself - otherwise the closure would never go out of scope, the destructor would never # get called, and the list of outstanding loaders would never get pruned. This way, the closure # holds a reference to the string version of its address, which is the only thing it really # needed anyway $me_loading_iterator_as_string = $loading_iterator . ''; $all_loading_iterators{$me_loading_iterator_as_string} = [ $me_loading_iterator_as_string, $normalized_rule, $object_sorter, $cached, \$underlying_context_objects_count, \$cached_objects_count, $context, ]; $is_multiple_loading_iterators = 1 if (keys(%all_loading_iterators) > 1); return $loading_iterator; } # end _create() sub DESTROY { my $self = shift; my $iter_data = $all_loading_iterators{$self}; if ($iter_data->[0] eq $self) { # that's me! # Items in the listref are: $loading_iterator_string, $rule, $object_sorter, $cached, # \$underlying_context_objects_count, \$cached_objects_count, $context my $context = $iter_data->[6]; if ($context and $context->monitor_query) { my $rule = $iter_data->[1]; my $count = ${$iter_data->[4]} + ${$iter_data->[5]}; $context->_log_query_for_rule($rule->subject_class_name, $rule, "QUERY: Query complete after returning $count object(s) for rule $rule."); $context->_log_done_elapsed_time_for_rule($rule); } delete $all_loading_iterators{$self}; $is_multiple_loading_iterators = 0 if (keys(%all_loading_iterators) < 2); } else { Carp::carp('A loading iterator went out of scope, but could not be found in the registered list of iterators'); } } # Used by the loading itertor to inject a newly loaded object into another # loading iterator's @$cached list. This is to handle the case where the user creates # an iterator which will load objects from the DB. Before all the data from that # iterator is read, another get() or iterator is created that covers (some of) the same # objects which get pulled into the object cache, and the second request is run to # completion. Since the underlying context iterator has been changed to never return # objects currently cached, the first iterator would have incorrectly skipped ome objects that # were not loaded when the first iterator was created, but later got loaded by the second. sub _inject_object_into_other_loading_iterators { my($self, $new_object, $iterator_to_skip) = @_; ITERATOR: foreach my $iter_name ( keys %all_loading_iterators ) { next if $iter_name eq $iterator_to_skip; # That's me! Don't insert into our own @$cached this way my($loading_iterator, $rule, $object_sorter, $cached) = @{$all_loading_iterators{$iter_name}}; if ($rule->evaluate($new_object)) { my $cached_list_len = @$cached; for(my $i = 0; $i < $cached_list_len; $i++) { my $cached_object = $cached->[$i]; next if $cached_object->isa('UR::DeletedRef'); my $comparison = $object_sorter->($new_object, $cached_object); if ($comparison < 0) { # The new object sorts sooner than this one. Insert it into the list splice(@$cached, $i, 0, $new_object); next ITERATOR; } elsif ($comparison == 0) { # This object is already in the list next ITERATOR; } } # It must go at the end... push @$cached, $new_object; } } # end foreach } # Reverse of _inject_object_into_other_loading_iterators(). Used when one iterator detects that # a previously loaded object no longer exists in the underlying context/datasource sub _remove_object_from_other_loading_iterators { my($self, $disappearing_object, $iterator_to_skip) = @_; ITERATOR: foreach my $iter_name ( keys %all_loading_iterators ) { next if(! defined $iterator_to_skip or ($iter_name eq $iterator_to_skip)); # That's me! Don't remove into our own @$cached this way my($loading_iterator, $rule, $object_sorter, $cached) = @{$all_loading_iterators{$iter_name}}; next if (defined($iterator_to_skip) and $loading_iterator eq $iterator_to_skip); # That's me! Don't insert into our own @$cached this way if ($rule->evaluate($disappearing_object)) { my $cached_list_len = @$cached; for(my $i = 0; $i < $cached_list_len; $i++) { my $cached_object = $cached->[$i]; next if $cached_object->isa('UR::DeletedRef'); my $comparison = $object_sorter->($disappearing_object, $cached_object); if ($comparison == 0) { # That's the one, remove it from the list splice(@$cached, $i, 1); next ITERATOR; } elsif ($comparison < 0) { # past the point where we expect to find this object next ITERATOR; } } } } # end foreach } # Returns true if any of the object's changed properites are keys # in the passed-in hashref. Used by the Loading Iterator to find out if # a change is one of the order-by properties of a bx sub _changed_property_in_hash { my($self,$object,$hash) = @_; foreach my $prop_name ( $object->_changed_property_names ) { return 1 if (exists $hash->{$prop_name}); } return; } 1; Transaction.pm000444023532023421 3007612121654173 17211 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::Transaction; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => __PACKAGE__, is => ['UR::Context'], has => [ begin_point => {}, end_point => {is_optional => 1}, # FIXME is this ever used anywhere? state => {}, # open, committed, rolled-back ], is_transactional => 1, ); our $log_all_changes = 0; our @change_log; our @open_transaction_stack; our $last_transaction_id = 0; sub delete { my $self = shift; #$DB::single = 1; $self->rollback; } sub begin { my $class = shift; my $id = $last_transaction_id++; #my $id = @open_transaction_stack; my $begin_point = @change_log; $log_all_changes = 1; my $last_trans = $open_transaction_stack[-1]; if ($last_trans and $last_trans != $UR::Context::current) { die "Current transaction does not match the top of the transaction stack!?" } $last_trans ||= $UR::Context::current; my $self = $class->create( id => $id, begin_point => $begin_point, state => "open", parent => $last_trans, @_ ); unless ($self) { Carp::confess("Failed to being transaction!"); } push @open_transaction_stack, $self; $UR::Context::current = $self; return $self; } sub log_change { my $this_class = shift; my ($object, $class, $id, $aspect, $undo_data) = @_; return if $class eq "UR::Change"; # wrappers (create/delete/load/unload/define) signal change also # and we undo the wrapper, thereby undoing these # -> ignore any signal from a method which is wrapped by another signalling method which gets undone return if ( $aspect eq "load" or $aspect eq "load_external" ); if (!ref($object) or $class eq "UR::Object::Index") { #print "skipping @_\n"; return; } if ($aspect eq "delete") { $undo_data = Data::Dumper::Dumper($object); } Carp::confess() if ref($class); my $change = UR::Change->create( id => scalar(@change_log)+1, changed_class_name => $class, changed_id => $id, changed_aspect => $aspect, undo_data => $undo_data, ); unless (ref($change)) { #$DB::single = 1; } push @change_log, $change; return $change; } sub has_changes { my $self = shift; my @changes = $self->get_changes(); return (@changes > 1 ? 1 : ()); } sub get_changes { my $self = shift; my $begin_point = $self->begin_point; my $end_point = $self->end_point || $#change_log; my @changes = @change_log[$begin_point..$end_point]; if (@_) { @changes = UR::Change->get(id => \@changes, @_) } else { return @changes; } } sub get_change_summary { # TODO: This should compress multiple changes to the same object as much as possible # Right now, it just omits the creation event for the transaction object itself. # -> should the creation of the transaction be part of it? # A: It should really be part of the prior transaction, and after commit/rollback # the nesting collapses. The @change_log should be _inside the transaction object, # or the change should contain a transaction id. The list can be destroyed on # rollback, or summarized on commit. my $self = shift; my @changes = grep { $_->changed_aspect !~ /^(load|define)$/ } $self->get_changes; shift @changes; # $self creation event return @changes; } sub rollback { my $self = shift; # Support calling as a class method: UR::Context::Transaction->rollback rolls back the current trans unless (ref($self)) { $self = $open_transaction_stack[-1]; unless ($self) { Carp::confess("No open transaction!? Cannot rollback."); } } if ($self->state ne "open") { Carp::confess("Cannot rollback a transaction that is " . $self->state . ".") } $self->__signal_change__('prerollback'); my $begin_point = $self->begin_point; unless ($self eq $open_transaction_stack[-1]) { # This is not the top transaction on the stack. # Rollback internally nested transactions in order from the end. my @transactions_with_begin_point = map { [ $_->begin_point, $_ ] } $self->class->get( begin_point => { operator => ">", value => $begin_point } ); my @later_transactions = map { $_->[1] } sort { $b->[0] <=> $a->[0] } @transactions_with_begin_point; for my $later_transaction (@later_transactions) { if ($later_transaction->isa("UR::DeletedRef")) { #$DB::single = 1; } $later_transaction->rollback; } } my $parent = $self->parent; if ($open_transaction_stack[-2] and $open_transaction_stack[-2] != $parent) { die "Parent transaction $parent is not below this one on the stack $open_transaction_stack[-2]?"; } # Reverse each change, starting from the most recent, and # ending with the creation of the transaction object itself. local $log_all_changes = 0; $self->__signal_change__('rollback', 1); my @changes_to_undo = reverse $self->get_changes(); my $transaction_change = pop @changes_to_undo; my $transaction = $transaction_change->changed_class_name->get($transaction_change->changed_id); unless ($self == $transaction && $transaction_change->changed_aspect eq 'create') { die "First change was not the creation of this transaction!"; } for my $change (@changes_to_undo) { if ($change == $changes_to_undo[0]) { # the transaction reverses itself in its own context, # but the removal of the transaction itself happens in the parent context $UR::Context::current = $parent; } $change->undo; $change->delete; } for my $change (@changes_to_undo) { unless($change->isa('UR::DeletedRef')) { Carp::confess("Failed to undo a change during transaction rollback."); } } $transaction_change->undo; $transaction_change->delete; $#change_log = $begin_point-1; unless($self->isa("UR::DeletedRef")) { #$DB::single = 1; Carp::confess("Failed to remove transaction during rollback."); } pop @open_transaction_stack; $UR::Context::current = $parent; return 1; } sub commit { my $self = shift; # Support calling as a class method: UR::Context::Transaction->commit commits the current transaction. unless (ref($self)) { $self = $open_transaction_stack[-1]; unless ($self) { Carp::confess("No open transaction!? Cannot commit."); } } if ($self->state ne "open") { Carp::confess("Cannot commit a transaction that is " . $self->state . ".") } unless ($open_transaction_stack[-1] == $self) { # TODO: decide if this should work like rollback, and commit nested transactions automatically Carp::confess("Cannot commit a transaction with open sub-transactions!"); } $self->__signal_change__('precommit'); unless ($self->changes_can_be_saved) { return; } $self->state("committed"); if ($self->state eq 'committed') { $self->__signal_change__('commit',1); } else { $self->__signal_change__('commit',0); } pop @open_transaction_stack; $UR::Context::current = $self->parent; return 1; } sub changes_can_be_saved { my $self = shift; # This is very similar to behavior in UR::Context::_sync_databases. The only # reason it isn't re-used from UR::Context is the desire to limit changed # objects to those changed within the transaction. # TODO: limit to objects that changed within transaction as to not duplicate # error checking unnecessarily. my @changed_objects = ( $self->all_objects_loaded('UR::Object::Ghost'), grep { $_->__changes__ } $self->all_objects_loaded('UR::Object') ); # This is primarily to catch custom validity logic in class overrides. my @invalid = grep { $_->__errors__ } @changed_objects; if (@invalid) { $self->display_invalid_data_for_save(\@invalid); return; } return 1; } sub execute { my $class = shift; Carp::confess("Attempt to call class method on instance. This is probably not what you want...") if ref $class; my $code = shift; my $transaction = $class->begin; my $result = eval($code->()); unless ($result) { $transaction->rollback; } if ($@) { die $@; } $transaction->commit; return $result; } sub execute_and_rollback { my $class = shift; Carp::confess("Attempt to call class method on instance. This is probably not what you want...") if ref $class; my $code = shift; my $transaction = $class->begin; my $result = eval($code->()); $transaction->rollback; if ($@) { die $@; } return $result; } 1; =pod =head1 NAME UR::Context::Transaction - API for software transactions =head1 SYNOPSIS my $o = Some::Obj->create(foo => 1); print "o's foo is ",$o->foo,"\n"; # prints 1 my $t = UR::Context::Transaction->begin(); $o->foo(4); print "In transaction, o's foo is ",$o->foo,"\n"; # prints 4 if (&should_we_commit()) { $t->commit(); print "Transaction committed, o's foo is ",$o->foo,"\n"; # prints 4 } else { $t->rollback(); print "Transaction rollback, o's foo is ",$o->foo,"\n"; # prints 1 } =head1 DESCRIPTION UR::Context::Transaction instances represent in-memory transactions as a diff of the contents of the object cache in the Process context. Transactions are nestable. Their instances exist in the object cache and are subject to the same scoping rules as other UR-based objects, meaning that they do not disappear mearly because the lexical variable they're assigned to goes out of scope. They must be explicitly disposed of via the commit or rollback methods. =head1 INHERITANCE UR::Context::Transaction is a subclass of UR::Context =head1 CONSTRUCTOR =over 4 =item begin $t = UR::Context::Transaction->begin(); Creates a new software transaction context to track changes to UR-based objects. As all activity to objects occurs in some kind of transaction context, the newly created transaction exists within whatever context was current before the call to begin(). =back =head1 METHODS =over 4 =item commit $t->commit(); Causes all objects with changes to save those changes back to the underlying context. =item rollback $t->rollback(); Causes all objects with changes to have those changes reverted to their state when the transaction began. Classes with properties whose meta-property is_transactional => 0 are not tracked within a transaction and will not be reverted. =item delete $t->delete(); delete() is a synomym for rollback =item has_changes $bool = $t->has_changes(); Returns true if any UR-based objects have changes within the transaction. =item get_changes @changes = $t->get_changes(); Return a list or L objects representing changes within the transaction. =back =head1 CLASS METHODS =over 4 =item execute $retval = UR::Context::Transaction->execute($coderef); Executes the coderef with no arguments, within an eval and a software transaction. If the coderef returns true, the transaction is committed. If it returns false, the transaction is rolled back. Finally the coderef's return value is returned to the caller. If the coderef throws an exception, it will be caught, the transaction rolled back, and the exception will be re-thrown with die(). =item execute_and_rollback UR::Context::Transaction->execute_and_rollback($coderef); Executes the coderef with no arguments, within an eval and a software transaction. Reguardless of the return value of the coderef, the transaction will be rolled back. If the coderef throws an exception, it will be caught, the transaction rolled back, and the exception will be re-thrown with die(). =back =head1 SEE ALSO L =cut Root.pm000444023532023421 472312121654174 15630 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::Root; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Context::Root', is => ['UR::Singleton', 'UR::Context'], is_abstract => 1, is_transactional => 1, doc => 'A base level context, representing the committed state of datasources external to the application.', ); # this is called automatically by UR.pm at the end of the module my $initialized = 0; sub _initialize_for_current_process { my $class = shift; if ($initialized) { die "Attempt to re-initialize the current process?"; } my $context_singleton_class = $ENV{UR_CONTEXT_ROOT} ||= 'UR::Context::DefaultRoot'; $class->set_current($context_singleton_class); } sub name { my $class = shift->_singleton_class_name; my ($name) = ($class =~ /^\w+?\:\:\w+?\:\:(\w+)$/); die "failed to parse name from $class!" unless $name; return lc($name); } sub get_current { #shift->_initialize_for_current_process() unless $initialized; #eval "sub get_current { \$ENV{UR_CONTEXT_ROOT} }"; return $ENV{UR_CONTEXT_ROOT}; } sub set_current { my $class = shift; my $value = shift; return $value if $value eq $ENV{UR_CONTEXT_ROOT}; $ENV{UR_CONTEXT_ROOT} = $value; #print "base context set to $value\n"; #print Carp::longmess(); eval { local $SIG{__DIE__}; local $SIG{__WARN__}; $ENV{UR_CONTEXT_ROOT}->class; }; if ($@) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This failed to compile:\n$@" } unless ($ENV{UR_CONTEXT_ROOT}->isa("UR::Context")) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This does not inherit from UR::Context." } unless ($ENV{UR_CONTEXT_ROOT}->__meta__) { die "The context at application initialization is set to " . $ENV{UR_CONTEXT_ROOT} . ".\n" . "This is not defined with UR::Object::Type metadata!" } # Initialize the bottom of the transaction stack if (@UR::Context::Transaction::open_transaction_stack > 1) { die "Cannot change the base context once transactions are in progress!" } return $value; } sub access_level { my $self = shift->_singleton_object; return "???"; } # sub has_changes { return } 1; DefaultRoot.pm000444023532023421 64412121654175 17114 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::DefaultRoot; use strict; use warnings; require UR; our $VERSION = "0.41"; # UR $VERSION; UR::Object::Type->define( class_name => 'UR::Context::DefaultRoot', is => ['UR::Context::Root'], doc => 'The base context used when no special base context is specified.', ); 1; =pod =head1 NAME UR::Context::DefaultRoot - The base context used when no special base context is specified =cut ObjectFabricator.pm000444023532023421 15451412121654175 20155 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context::ObjectFabricator; use strict; use warnings; use Scalar::Util; use UR::Context; our $VERSION = "0.41"; # UR $VERSION; # A helper package for UR::Context to keep track of the subrefs used # to create objects from database data # These are normal Perl objects, not UR objects, so they get # regular refcounting and scoping our @CARP_NOT = qw( UR::Context ); my %all_object_fabricators; sub _create { my $class = shift; my %params = @_; unless ($params{'fabricator'} and ref($params{'fabricator'}) eq 'CODE') { Carp::croak("UR::Context::ObjectFabricator::create requires a subroutine ref for the 'fabricator' parameter"); } unless ($params{'context'} and ref($params{'context'}) and $params{'context'}->isa('UR::Context')) { Carp::croak("UR::Context::ObjectFabricator::create requires a UR::Context object for the 'context' parameter"); } my $self = bless {}, $class; $self->{'fabricator'} = $params{'fabricator'}; $self->{'context'} = $params{'context'}; $self->{'all_params_loaded'} = $params{'all_params_loaded'} || {}; $self->{'in_clause_values'} = $params{'in_clause_values'} || {}; $all_object_fabricators{$self} = $self; Scalar::Util::weaken($all_object_fabricators{$self}); return $self; } sub create_for_loading_template { my($fab_class, $context, $loading_template, $query_plan, $rule, $rule_template, $values, $dsx) = @_; my @values = @$values; my $class_name = $loading_template->{final_class_name}; #$class_name or Carp::croak("No final_class_name in loading template?"); unless ($class_name) { #Carp::carp("No final_class_name in loading template for rule $rule"); return; # This join doesn't result in an object? - i think this happens when you do a get() with -hints } my $class_meta = $class_name->__meta__; my $class_data = $dsx->_get_class_data_for_loading($class_meta); my $class = $class_name; my $ghost_class = $class_data->{ghost_class}; my $sub_classification_meta_class_name = $class_data->{sub_classification_meta_class_name}; my $subclassify_by = $class_data->{subclassify_by}; my $sub_classification_method_name = $class_data->{sub_classification_method_name}; # FIXME, right now, we don't have a rule template for joined entities... my $rule_template_id = $query_plan->{rule_template_id}; my $rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc}; my $rule_template_id_without_recursion_desc = $query_plan->{rule_template_id_without_recursion_desc}; my $rule_matches_all = $query_plan->{rule_matches_all}; my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only}; my $rule_specifies_id = $query_plan->{rule_specifies_id}; my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype}; my $recursion_desc = $query_plan->{recursion_desc}; my $recurse_property_on_this_row = $query_plan->{recurse_property_on_this_row}; my $recurse_property_referencing_other_rows = $query_plan->{recurse_property_referencing_other_rows}; my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'}; my $rule_id = $rule->id; my $rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values); my $loading_base_object; if ($loading_template == $query_plan->{loading_templates}[0]) { $loading_base_object = 1; } else { $loading_base_object = 0; $needs_further_boolexpr_evaluation_after_loading = 0; } my %subclass_is_safe_for_re_bless; my %subclass_for_subtype_name; my %recurse_property_value_found; my @property_names = @{ $loading_template->{property_names} }; my @id_property_names = @{ $loading_template->{id_property_names} }; my @column_positions = @{ $loading_template->{column_positions} }; my @id_positions = @{ $loading_template->{id_column_positions} }; my $multi_column_id = (@id_positions > 1 ? 1 : 0); my $composite_id_resolver = $class_meta->get_composite_id_resolver; # The old way of specifying that some values were constant for all objects returned # by a get(). The data source would wrap the method that builds the loading template # and wedge in some constant_property_names. The new way is to add columns to the # loading template, and then add the values onto the list returned by the data source # iterator. my %initial_object_data; if ($loading_template->{constant_property_names}) { my @constant_property_names = @{ $loading_template->{constant_property_names} }; my @constant_property_values = map { $rule->value_for($_) } @constant_property_names; @initial_object_data{@constant_property_names} = @constant_property_values; } my $rule_class_name = $rule_template->subject_class_name; my $template_id = $rule_template->id; my $load_class_name = $class; # $rule can contain params that may not apply to the subclass that's currently loading. # define_boolexpr() in array context will return the portion of the rule that actually applies #my($load_rule, undef) = $load_class_name->define_boolexpr($rule->params_list); my($load_rule, @extra_params) = UR::BoolExpr->resolve($load_class_name, $rule->params_list); my $load_rule_id = $load_rule->id; my $load_template_id = $load_rule->template_id; my @rule_properties_with_in_clauses = grep { $rule_template_without_recursion_desc->operator_for($_) eq 'in' } $rule_template_without_recursion_desc->_property_names; my($rule_template_without_in_clause,$rule_template_id_without_in_clause,%in_clause_values,@all_rule_property_names); my $do_record_in_all_params_loaded = 1; if (@rule_properties_with_in_clauses) { $rule_template_id_without_in_clause = $rule_template_without_recursion_desc->id; foreach my $property_name ( @rule_properties_with_in_clauses ) { # FIXME - removing and re-adding the filter should have the same effect as the substitute below, # but the two result in different rules in the end. #$rule_template_without_in_clause = $rule_template_without_in_clause->remove_filter($property_name); #$rule_template_without_in_clause = $rule_template_without_in_clause->add_filter($property_name); $rule_template_id_without_in_clause =~ s/($property_name) in/$1/; } $rule_template_without_in_clause = UR::BoolExpr::Template->get($rule_template_id_without_in_clause); # Make a note of all the values in the in-clauses. As the objects get returned from the # data source, we'll remove these notes. Anything that's left by the time the iterator is # finalized must be values that matched nothing. Then, finalize can put data in # all_params_loaded showing it matches nothing my %rule_properties_with_in_clauses = map { $_ => 1 } @rule_properties_with_in_clauses; @all_rule_property_names = $rule_template_without_in_clause->_property_names; foreach my $property ( @rule_properties_with_in_clauses ) { my $values_for_in_clause = $rule_without_recursion_desc->value_for($property); if (@$values_for_in_clause > 100) { $do_record_in_all_params_loaded = 0; next; } my @other_values = map { exists $rule_properties_with_in_clauses{$_} ? undef # placeholder filled in below : $rule_without_recursion_desc->value_for($_) } $rule_template_without_in_clause->_property_names; my $position_for_this_property = $rule_template_without_in_clause->value_position_for_property_name($property); # If the number of items in the in-clause is over this number, then don't bother recording # the template-id/rule-id, since searching the list to see if this query has been done before # is going to take longer than just re-doing the query foreach my $value ( @$values_for_in_clause ) { $value = '' if (!defined $value); $other_values[$position_for_this_property] = $value; my $rule_with_this_in_property = $rule_template_without_in_clause->get_rule_for_values(@other_values); $in_clause_values{$property}->{$value} = [$rule_template_id_without_in_clause, $rule_with_this_in_property->id]; } } } # This is a local copy of what we want to put in all_params_loaded, when the object fabricator is # finalized my $local_all_params_loaded = {}; my($hints_or_delegation,$delegations_with_no_objects); if (!$loading_base_object) { ($hints_or_delegation,$delegations_with_no_objects) = $fab_class->_resolve_delegation_data($rule,$loading_template,$query_plan,$local_all_params_loaded); } my $update_apl_for_loaded_object = sub { my $pending_db_object = shift; # Make a note in all_params_loaded (essentially, the query cache) that we've made a # match on this rule, and some equivalent rules if ($loading_base_object and not $rule_specifies_id) { if ($do_record_in_all_params_loaded) { if ($rule_class_name ne $load_class_name and scalar(@extra_params) == 0) { $pending_db_object->{__load}->{$load_template_id}{$load_rule_id}++; $UR::Context::all_params_loaded->{$load_template_id}{$load_rule_id} = undef; $local_all_params_loaded->{$load_template_id}{$load_rule_id}++; } $pending_db_object->{__load}->{$template_id}{$rule_id}++; $UR::Context::all_params_loaded->{$template_id}{$rule_id} = undef; $local_all_params_loaded->{$template_id}{$rule_id}++; } if (@rule_properties_with_in_clauses) { # FIXME - confirm that all the object properties are filled in at this point, right? #my @values = @$pending_db_object{@rule_properties_with_in_clauses}; my @values = @$pending_db_object{@all_rule_property_names}; my $r = $rule_template_without_in_clause->get_normalized_rule_for_values(@values); my $r_id = $r->id; $UR::Context::all_params_loaded->{$rule_template_id_without_in_clause}{$r_id} = undef; $local_all_params_loaded->{$rule_template_id_without_in_clause}{$r_id}++; # remove the notes about these in-clause values since they matched something no warnings; # undef treated as an empty string below foreach my $property (@rule_properties_with_in_clauses) { my $value = $pending_db_object->{$property}; delete $in_clause_values{$property}->{$value}; } } } }; my $fabricator_obj; # filled in after the closure definition my $object_fabricator = sub { my $next_db_row = $_[0]; # If all the columns for this object are undef, then this doesn't encode an actual # object, it's a result of a left join that matched nothing my $values_exist; foreach my $column ( @column_positions ) { if (defined($next_db_row->[$column])) { $values_exist = 1; last; } } if (!$loading_base_object and !$values_exist and $delegations_with_no_objects) { my $templates_and_rules = $fab_class->_lapl_data_for_delegation_data($delegations_with_no_objects, $next_db_row); while ( my($template_id, $rule_id) = each %$templates_and_rules) { $local_all_params_loaded->{$template_id}->{$rule_id} = 0; $UR::Context::all_params_loaded->{$template_id}->{$rule_id} = 0; } return; } my $pending_db_object_data = { %initial_object_data }; @$pending_db_object_data{@property_names} = @$next_db_row[@column_positions]; # resolve id my $pending_db_object_id; if ($multi_column_id) { $pending_db_object_id = $composite_id_resolver->(@$pending_db_object_data{@id_property_names}) } else { $pending_db_object_id = $pending_db_object_data->{$id_property_names[0]}; } unless (defined $pending_db_object_id) { return undef; Carp::confess( "no id found in object data for $class_name?\n" . Data::Dumper::Dumper($pending_db_object_data) ); } my $pending_db_object; # skip if this object has been deleted but not committed do { no warnings; if ($UR::Context::all_objects_loaded->{$ghost_class}{$pending_db_object_id}) { return; #$pending_db_object = undef; #redo; } }; # Handle the object based-on whether it is already loaded in the current context. if ($pending_db_object = $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id}) { $context->__merge_db_data_with_existing_object($class, $pending_db_object, $pending_db_object_data, \@property_names); if ($loading_base_object and $needs_further_boolexpr_evaluation_after_loading and not $rule->evaluate($pending_db_object) ) { return; } $update_apl_for_loaded_object->($pending_db_object); } else { # Handle the case in which the object is completely new in the current context. # Create a new object for the resultset row $pending_db_object = bless { %$pending_db_object_data, id => $pending_db_object_id }, $class; $pending_db_object->{db_committed} = $pending_db_object_data; # determine the subclass name for classes which automatically sub-classify my $subclass_name; if ( ( $sub_classification_method_name or $subclassify_by or $sub_classification_meta_class_name ) and (ref($pending_db_object) eq $class) # not already subclased ) { if ($sub_classification_method_name) { $subclass_name = $class->$sub_classification_method_name($pending_db_object); unless ($subclass_name) { my $pending_obj_id = eval { $pending_db_object->id }; Carp::confess( "Object with id '$pending_obj_id' loaded as abstract class $class failed to subclassify itself using method " . $sub_classification_method_name ); } } elsif ($sub_classification_meta_class_name) { # Group objects requiring reclassification by type, # and catch anything which doesn't need reclassification. my $subtype_name = $pending_db_object->$subclassify_by; $subclass_name = $subclass_for_subtype_name{$subtype_name}; unless ($subclass_name) { my $type_obj = $sub_classification_meta_class_name->get($subtype_name); unless ($type_obj) { # The base type may give the final subclass, or an intermediate # either choice has trade-offs, but we support both. # If an intermediate subclass is specified, that subclass # will join to a table with another field to indicate additional # subclassing. This means we have to do this part the hard way. # TODO: handle more than one level. my @all_type_objects = $sub_classification_meta_class_name->get(); for my $some_type_obj (@all_type_objects) { my $some_subclass_name = $some_type_obj->subclass_name($class); unless (UR::Object::Type->get($some_subclass_name)->is_abstract) { next; } my $some_subclass_meta = $some_subclass_name->__meta__; my $some_subclass_type_class = $some_subclass_meta->sub_classification_meta_class_name; if ($type_obj = $some_subclass_type_class->get($subtype_name)) { # this second-tier subclass works last; } else { # try another subclass, and check the subclasses under it #print "skipping $some_subclass_name: no $subtype_name for $some_subclass_type_class\n"; } } } if ($type_obj) { $subclass_name = $type_obj->subclass_name($class); } else { warn "Failed to find $class_name sub-class for type '$subtype_name'!"; $subclass_name = $class_name; } unless ($subclass_name) { Carp::confess( "Failed to sub-classify $class using " . $type_obj->class . " '" . $type_obj->id . "'" ); } $subclass_name->class; } $subclass_for_subtype_name{$subtype_name} = $subclass_name; } else { $subclass_name = $pending_db_object->$subclassify_by; unless ($subclass_name) { Carp::croak("Failed to sub-classify $class while loading; calling method " . "'$subclassify_by' returned false. Relevant object data: " . Data::Dumper::Dumper($pending_db_object)); } } # note: we check this again with the real base class, but this keeps junk objects out of the core hash unless ($subclass_name->isa($class)) { # We may have done a load on the base class, and not been able to use properties to narrow down to the correct subtype. # The resultset returned more data than we needed, and we're filtering out the other subclasses here. return; } } else { # regular, non-subclassifier $subclass_name = $class; } # store the object # note that we do this on the base class even if we know it's going to be put into a subclass below $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id} = $pending_db_object; $UR::Context::all_objects_cache_size++; # If we're using a light cache, weaken the reference. if ($UR::Context::light_cache and substr($class,0,5) ne 'App::') { Scalar::Util::weaken($UR::Context::all_objects_loaded->{$class_name}->{$pending_db_object_id}); } $update_apl_for_loaded_object->($pending_db_object); my $boolexpr_evaluated_ok; if ($subclass_name eq $class) { # This object doesn't need additional subclassing # Signal that the object has been loaded # NOTE: until this is done indexes cannot be used to look-up an object $pending_db_object->__signal_change__('load'); } else { # we did this above, but only checked the base class my $subclass_ghost_class = $subclass_name->ghost_class; if ($UR::Context::all_objects_loaded->{$subclass_ghost_class}{$pending_db_object_id}) { # We put it in the object cache a few lines above. # FIXME - why not wait until we know we're keeping it before putting it in there? delete $UR::Context::all_objects_loaded->{$class}{$pending_db_object_id}; $UR::Context::all_objects_cache_size--; return; #$pending_db_object = undef; #redo; } my $re_bless = $subclass_is_safe_for_re_bless{$subclass_name}; if (not defined $re_bless) { $re_bless = $dsx->_class_is_safe_to_rebless_from_parent_class($subclass_name, $class); $re_bless ||= 0; $subclass_is_safe_for_re_bless{$subclass_name} = $re_bless; } my $loading_info; if (!$re_bless) { # This object cannot just be re-classified into a subclass because the subclass joins to additional tables. # We'll make a parallel iterator for each subclass we encounter. # Note that we let the calling db-based iterator do that, so that if multiple objects on the row need # sub-classing, we do them all at once. # Decrement all of the param_keys it is using. if ($loading_base_object) { $loading_info = $dsx->_get_object_loading_info($pending_db_object); $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } #$pending_db_object->unload; delete $UR::Context::all_objects_loaded->{$class}->{$pending_db_object_id}; if ($loading_base_object) { $dsx->_record_that_loading_has_occurred($loading_info); } # NOTE: we're returning a class name instead of an object # this tells the caller to re-do the entire row using a subclass to get the real data. # Hack? Probably so... return $subclass_name; } # Performance shortcut. # These need to be subclassed, but there is no additional data to load. # Just remove from the object cache, rebless to the proper subclass, and # re-add to the object cache my $already_loaded = $subclass_name->is_loaded($pending_db_object_id); my $different; my $merge_exception; if ($already_loaded) { eval { $different = $context->__merge_db_data_with_existing_object($class, $already_loaded, $pending_db_object_data, \@property_names) }; $merge_exception = $@; } if ($already_loaded and !$different and !$merge_exception) { if ($pending_db_object == $already_loaded) { Carp::croak("An object of type ".$already_loaded->class." with ID '".$already_loaded->id ."' was just loaded, but already exists in the object cache in the proper subclass"); } if ($loading_base_object) { # Get our records about loading this object $loading_info = $dsx->_get_object_loading_info($pending_db_object); # Transfer the load info for the load we _just_ did to the subclass too. my $subclassified_template = $rule_template->sub_classify($subclass_name); $loading_info->{$subclassified_template->id} = $loading_info->{$template_id}; $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } # This will wipe the above data from the object and the contex... delete $UR::Context::all_objects_loaded->{$class}->{$pending_db_object_id}; if ($loading_base_object) { # ...now we put it back for both. $dsx->_add_object_loading_info($already_loaded, $loading_info); $dsx->_record_that_loading_has_occurred($loading_info); } bless($pending_db_object,'UR::DeletedRef'); $pending_db_object = $already_loaded; } else { if ($loading_base_object) { my $subclassified_template = $rule_template->sub_classify($subclass_name); $loading_info = $dsx->_get_object_loading_info($pending_db_object); $dsx->_record_that_loading_has_occurred($loading_info); $loading_info->{$subclassified_template->id} = delete $loading_info->{$template_id}; $loading_info = $dsx->_reclassify_object_loading_info_for_new_class($loading_info,$subclass_name); } my $prev_class_name = $pending_db_object->class; #my $id = $pending_db_object->id; #$pending_db_object->__signal_change__("unload"); delete $UR::Context::all_objects_loaded->{$prev_class_name}->{$pending_db_object_id}; delete $UR::Context::all_objects_are_loaded->{$prev_class_name}; if ($merge_exception) { # Now that we've removed traces of the incorrectly-subclassed $pending_db_object, # we can pass up any exception generated in __merge_db_data_with_existing_object Carp::croak($merge_exception); } if ($already_loaded) { # The new object should replace the old object. Since other parts of the user's program # may have references to this object, we need to copy the values from the new object into # the existing cached object bless($pending_db_object,'UR::DeletedRef'); $pending_db_object = $already_loaded; } else { # This is a completely new object $UR::Context::all_objects_loaded->{$subclass_name}->{$pending_db_object_id} = $pending_db_object; } bless $pending_db_object, $subclass_name; $pending_db_object->__signal_change__("load"); $dsx->_add_object_loading_info($pending_db_object, $loading_info); $dsx->_record_that_loading_has_occurred($loading_info); } # the object may no longer match the rule after subclassifying... if ($needs_further_boolexpr_evaluation_after_loading and $loading_base_object and not $rule->evaluate($pending_db_object) ) { #print "Object does not match rule!" . Dumper($pending_db_object,[$rule->params_list]) . "\n"; #$rule->evaluate($pending_db_object); return; } else { $boolexpr_evaluated_ok = 1; } } # end of sub-classification code if ( $loading_base_object and $needs_further_boolexpr_evaluation_after_loading and ( ! $boolexpr_evaluated_ok ) and ( ! $rule->evaluate($pending_db_object) ) ) { return; } } # end handling newly loaded objects # If the rule had hints, mark that we loaded those things too, in all_params_loaded if ($hints_or_delegation) { my $templates_and_rules = $fab_class->_lapl_data_for_delegation_data($hints_or_delegation, $next_db_row, $pending_db_object); while ( my($template_id, $rule_id) = each %$templates_and_rules) { $local_all_params_loaded->{$template_id}->{$rule_id}++; $UR::Context::all_params_loaded->{$template_id}->{$rule_id} = undef; } } # note all of the joins which follow this object as having been "done" if (my $next_joins = $loading_template->{next_joins}) { if (0) { # disabled until a fully reframed query is the basis for these joins for my $next_join (@$next_joins) { my ($bxt_id, $values, $value_position_property_name) = @$next_join; for (my $n = 0; $n < @$value_position_property_name; $n+=2) { my $pos = $value_position_property_name->[$n]; my $name = $value_position_property_name->[$n+1]; $values->[$pos] = $pending_db_object->$name; } my $bxt = UR::BoolExpr::Template::And->get($bxt_id); my $bx = $bxt->get_rule_for_values(@$values); $UR::Context::all_params_loaded->{$bxt->{id}}->{$bx->{id}} = undef; $local_all_params_loaded->{$bxt->{id}}->{$bx->{id}}++; print "remembering $bx\n"; } } } # When there is recursion in the query, we record data from each # recursive "level" as though the query was done individually. if ($recursion_desc and $loading_base_object) { # if we got a row from a query, the object must have # a db_committed or db_saved_committed my $dbc = $pending_db_object->{db_committed} || $pending_db_object->{db_saved_uncommitted}; Carp::croak("Loaded database data has no save data for $class id ".$pending_db_object->id .". Something bad happened.".Data::Dumper::Dumper($pending_db_object)) unless $dbc; my $value_by_which_this_object_is_loaded_via_recursion = $dbc->{$recurse_property_on_this_row}; my $value_referencing_other_object = $dbc->{$recurse_property_referencing_other_rows}; $value_referencing_other_object = '' unless (defined $value_referencing_other_object); unless ($recurse_property_value_found{$value_referencing_other_object}) { # This row points to another row which will be grabbed because the query is hierarchical. # Log the smaller query which would get the hierarchically linked data directly as though it happened directly. $recurse_property_value_found{$value_referencing_other_object} = 1; # note that the direct query need not be done again my $equiv_rule = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_referencing_other_object, ); my $equiv_rule_id = $equiv_rule->id; my $equiv_template_id = $equiv_rule->template_id; # note that the recursive query need not be done again my $equiv_rule_2 = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_referencing_other_object, -recurse => $recursion_desc, ); my $equiv_rule_id_2 = $equiv_rule_2->id; my $equiv_template_id_2 = $equiv_rule_2->template_id; # For any of the hierarchically related data which is already loaded, # note on those objects that they are part of that query. These may have loaded earlier in this # query, or in a previous query. Anything NOT already loaded will be hit later by the if-block below. my @subset_loaded = $class->is_loaded($recurse_property_on_this_row => $value_referencing_other_object); $UR::Context::all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = undef; $UR::Context::all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = undef; $local_all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = scalar(@subset_loaded); $local_all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = scalar(@subset_loaded); for my $pending_db_object (@subset_loaded) { $pending_db_object->{__load}->{$equiv_template_id}->{$equiv_rule_id}++; $pending_db_object->{__load}->{$equiv_template_id_2}->{$equiv_rule_id_2}++; } } # NOTE: if it were possible to use undef values in a connect-by, this could be a problem # however, connect by in UR is always COL = COL, which would always fail on NULLs. if (defined($value_by_which_this_object_is_loaded_via_recursion) and $recurse_property_value_found{$value_by_which_this_object_is_loaded_via_recursion}) { # This row was expected because some other row in the hierarchical query referenced it. # Up the object count, and note on the object that it is a result of this query. my $equiv_rule = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_by_which_this_object_is_loaded_via_recursion, ); my $equiv_rule_id = $equiv_rule->id; my $equiv_template_id = $equiv_rule->template_id; # note that the recursive query need not be done again my $equiv_rule_2 = UR::BoolExpr->resolve_normalized( $class, $recurse_property_on_this_row => $value_by_which_this_object_is_loaded_via_recursion, -recurse => $recursion_desc ); my $equiv_rule_id_2 = $equiv_rule_2->id; my $equiv_template_id_2 = $equiv_rule_2->template_id; $UR::Context::all_params_loaded->{$equiv_template_id}->{$equiv_rule_id} = undef; $UR::Context::all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2} = undef; $local_all_params_loaded->{$equiv_template_id}->{$equiv_rule_id}++; $local_all_params_loaded->{$equiv_template_id_2}->{$equiv_rule_id_2}++; $pending_db_object->{__load}->{$equiv_template_id}->{$equiv_rule_id}++; $pending_db_object->{__load}->{$equiv_template_id_2}->{$equiv_rule_id_2}++; } } # end of handling recursion return $pending_db_object; }; # end of per-class object fabricator Sub::Name::subname("UR::Context::__object_fabricator(closure)__ ($class_name)", $object_fabricator); # remember all the changes to $UR::Context::all_params_loaded that should be made. # This fixes the problem where you create an iterator for a query, read back some of # the items, but not all, then later make the same query. The old behavior made # entries in all_params_loaded as objects got loaded from the DB, so that at the time # the second query is made, UR::Context::_cache_is_complete_for_class_and_normalized_rule() # sees there are entries in all_params_loaded, and so reports yes, the cache is complete, # and the second query only returns the objects that were loaded during the first query. # # The new behavior builds up changes to be made to all_params_loaded, and someone # needs to call $object_fabricator->finalize() to apply these changes $fabricator_obj = $fab_class->_create(fabricator => $object_fabricator, context => $context, all_params_loaded => $local_all_params_loaded, in_clause_values => \%in_clause_values); return $fabricator_obj; } # Given the data created in _resolve_delegation_data (rule templates and values/valuerefs) # return a hash of template IDs => rule IDs that need to be manipulated in local_all_params_loaded sub _lapl_data_for_delegation_data { my($fab_class, $delegation_data_list, $next_db_row, $pending_db_object) = @_; my %tmpl_and_rules; #$DB::single=1; foreach my $delegation_data ( @$delegation_data_list ) { my $value_sources = $delegation_data->[0]; my $rule_tmpl = $delegation_data->[1]; my @values; foreach my $value_source ( @$value_sources ) { if (! ref($value_source)) { push @values, $value_source; } elsif (Scalar::Util::looks_like_number($$value_source)) { push @values, $next_db_row->[$$value_source]; } elsif ($pending_db_object) { my $method_name = $$value_source; my $result = eval { $pending_db_object->$method_name }; push @values, $result; } else { Carp::croak("Can't resolve value for '".$$value_source."' in delegation data when there is no object involved"); } } my $rule = $rule_tmpl->get_normalized_rule_for_values(@values); $tmpl_and_rules{$rule_tmpl->id} = $rule->id; } return \%tmpl_and_rules; } # This is used by fabricators created as a result of filters or hints on delegated properties # of the primary object to pre-calculate rule templates and value sources that can be combined # with these templates to make rules. The resulting template and rule IDs are then plugged into # all_params_loaded to indicate these related objects are loaded so that subsequent queries # will not hit the data sources. sub _resolve_delegation_data { my($fab_class,$rule,$loading_template,$query_plan,$local_all_params_loaded) = @_; my $rule_template = $rule->template; my $query_class_meta = $rule_template->subject_class_name->__meta__; my %hints; if ($rule_template->hints) { $hints{$_} = 1 foreach(@{ $rule_template->hints }); } my %delegations; if (@{ $query_plan->{'joins'}} ) { foreach my $delegated_property_name ( $rule_template->_property_names ) { my $delegated_property_meta = $query_class_meta->property_meta_for_name($delegated_property_name); next unless ($delegated_property_meta and $delegated_property_meta->is_delegated); $delegations{$delegated_property_name} = 1; } } my $this_object_num = $loading_template->{'object_num'}; my $join = $query_plan->_get_alias_join($loading_template->{'table_alias'}); return unless $join; # would this ever be false? return unless ($join->{'foreign_class'} eq $loading_template->{'data_class_name'}); # sanity check my $delegated_property_meta; # Find out which delegation property was responsible for this object being loaded DELEGATIONS: foreach my $delegation ((keys %hints), (keys %delegations)) { my $query_property_meta = $query_class_meta->property_meta_for_name($delegation); next DELEGATIONS unless $query_property_meta; my @joins_from_delegation = $query_property_meta->_resolve_join_chain(); foreach my $join_from_delegation ( @joins_from_delegation ) { if ($join_from_delegation->id eq $join->id) { $delegated_property_meta = $query_property_meta; last DELEGATIONS; } } } return unless $delegated_property_meta; my $delegated_property_name = $delegated_property_meta->property_name; return if $join->destination_is_all_id_properties(); my @template_filter_names = @{$join->{'foreign_property_names'}}; my %template_filter_values; foreach my $name ( @template_filter_names ) { my $column_num = $query_plan->column_index_for_class_property_and_object_num( $join->{'foreign_class'}, $name, $this_object_num); if (defined $column_num) { $template_filter_values{$name} = \$column_num; } else { my $prop_name = $name; $template_filter_values{$name} = \$prop_name; } } if ($delegations{$delegated_property_name}) { my $delegation_final_property_meta = $delegated_property_meta->final_property_meta; if ($delegation_final_property_meta and $delegation_final_property_meta->class_name eq $join->{'foreign_class'} ) { # This delegation points to (or at least through) this join's foreign class # We'll note that these related objects were loaded as a result of being # connected to the primary object by this value, and filtered by the # delegation property's value my $delegation_final_property_name = $delegation_final_property_meta->property_name; my $column_num = $query_plan->column_index_for_class_property_and_object_num( $join->{'foreign_class'}, $delegation_final_property_name, $this_object_num); push @template_filter_names, $delegation_final_property_name; if ($delegation_final_property_meta->column_name and ! defined ($column_num)) { # sanity check Carp::carp("Could not determine column offset in result set for property " . "$delegation_final_property_name of class " . $join->{'foreign_class'} . " even though it has column_name " . $delegation_final_property_meta->column_name); $column_num = undef; } if (defined $column_num) { $template_filter_values{$delegation_final_property_name} = \$column_num; } else { $template_filter_values{$delegation_final_property_name} = \$delegation_final_property_name; } } } # For missing objects, ie. a left join was done and it matched nothing my @missing_prop_names; my %missing_values; for (my $i = 0; $i < @{ $join->{'foreign_property_names'}}; $i++) { # we're using the source class/property here because we're going to denote that a value # of the source class of the join matched nothing my $prop_name = $join->{'foreign_property_names'}->[$i]; push @missing_prop_names, $prop_name; my $source_class = $join->{'source_class'}; my $source_prop_name = $join->{'source_property_names'}->[$i]; my $column_num; if( my($actual_prop_meta) = $source_class->__meta__->_concrete_property_meta_for_class_and_name($source_prop_name) ) { $column_num = $query_plan->column_index_for_class_and_property_before_object_num($actual_prop_meta->class_name, $actual_prop_meta->property_name, $this_object_num); } if (defined $column_num) { $missing_values{$prop_name} = \$column_num; } else { Carp::croak("Can't determine resultset column for $source_class property $source_prop_name for rule $rule"); } } if ($join->{'where'}) { for (my $i = 0; $i < @{$join->{'where'}}; $i += 2) { my $where_prop = $join->{'where'}->[$i]; push @template_filter_names, $where_prop; push @missing_prop_names, $where_prop; my $pos = index($where_prop, ' '); if ($pos != -1) { # the key is "propname op" $where_prop = substr($where_prop,0,$pos); } my $where_value = $join->{'where'}->[$i+1]; $template_filter_values{$where_prop} = $where_value; $missing_values{$where_prop} = $where_value; } } my $missing_rule_tmpl = UR::BoolExpr::Template->resolve($join->{'foreign_class'}, @missing_prop_names)->get_normalized_template_equivalent; my $related_rule_tmpl = UR::BoolExpr::Template->resolve($join->{'foreign_class'}, @template_filter_names)->get_normalized_template_equivalent; my(@hints_or_delegation, @delegations_with_no_objects); # Items in the first listref can be one of three things: # 1) a reference to an integer - meaning retrieve the value from this column in the result set # 2) a reference to a string - meaning retrieve the value from the object usign this as a property name # 3) a string - meaning this is a literal value to fill in directly # The second item is a rule template we'll be feeding these values in to my @template_filter_values = @template_filter_values{$related_rule_tmpl->_property_names}; push @hints_or_delegation, [ \@template_filter_values, $related_rule_tmpl]; my @missing_values = @missing_values{$missing_rule_tmpl->_property_names}; push @delegations_with_no_objects, [\@missing_values, $missing_rule_tmpl]; return (\@hints_or_delegation, \@delegations_with_no_objects); } sub all_object_fabricators { return values %all_object_fabricators; } # simple accessors sub fabricator { my $self = shift; return $self->{'fabricator'}; } sub context { my $self = shift; return $self->{'context'}; } sub all_params_loaded { my $self = shift; return $self->{'all_params_loaded'}; } sub in_clause_values { my $self = shift; return $self->{'in_clause_values'}; } # call the object fabricator closure sub fabricate { my $self = shift; &{$self->{'fabricator'}}; } # Returns true if this fabricator has loaded an object matching this boolexpr sub is_loading_in_progress_for_boolexpr { my $self = shift; my $boolexpr = shift; my $template_id = $boolexpr->template_id; # FIXME should it use is_subsest_of here? return unless exists $self->{'all_params_loaded'}->{$template_id}; return unless exists $self->{'all_params_loaded'}->{$template_id}->{$boolexpr->id}; return 1; } # UR::Contect::_abandon_object calls this to forget about loading an object sub delete_from_all_params_loaded { my($self,$template_id,$boolexpr_id) = @_; return unless ($template_id and $boolexpr_id); my $all_params_loaded = $self->all_params_loaded; return unless $all_params_loaded; return unless exists($all_params_loaded->{$template_id}); delete $all_params_loaded->{$template_id}->{$boolexpr_id}; } sub finalize { my $self = shift; $self->apply_all_params_loaded(); delete $all_object_fabricators{$self}; $self->{'all_params_loaded'} = undef; } sub apply_all_params_loaded { my $self = shift; my $local_all_params_loaded = $self->{'all_params_loaded'}; my @template_ids = keys %$local_all_params_loaded; foreach my $template_id ( @template_ids ) { my @rule_ids = keys %{$local_all_params_loaded->{$template_id}}; foreach my $rule_id ( @rule_ids ) { my $val = $local_all_params_loaded->{$template_id}->{$rule_id}; next unless exists $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; # Has unload() removed this one earlier? $UR::Context::all_params_loaded->{$template_id}->{$rule_id} += $val; } } # Anything left in here is in-clause values that matched nothing. Make a note in # all_params_loaded showing that so later queries for those values won't hit the # data source my $in_clause_values = $self->{'in_clause_values'}; my @properties = keys %$in_clause_values; foreach my $property ( @properties ) { my @values = keys %{$in_clause_values->{$property}}; foreach my $value ( @values ) { my $data = $in_clause_values->{$property}->{$value}; $UR::Context::all_params_loaded->{$data->[0]}->{$data->[1]} = 0; } } $self->{'all_params_loaded'} = {}; } sub DESTROY { my $self = shift; # Don't apply the changes. Maybe the importer closure just went out of scope before # it read all the data my $local_all_params_loaded = $self->{'all_params_loaded'}; if ($local_all_params_loaded) { # finalize wasn't called on this iterator; maybe the importer closure went out # of scope before it read all the data. # Conditionally apply the changes from the local all_params_loaded. If the Context's # all_params_loaded is defined, then another query has successfully run to # completion, and we should add our data to it. Otherwise, we're the only query like # this and all_params_loaded should be cleaned out foreach my $template_id ( keys %$local_all_params_loaded ) { while(1) { my($rule_id, $val) = each %{$local_all_params_loaded->{$template_id}}; last unless $rule_id; if (defined $UR::Context::all_params_loaded->{$template_id}->{$rule_id}) { $UR::Context::all_params_loaded->{$template_id}->{$rule_id} += $val; } else { delete $UR::Context::all_params_loaded->{$template_id}->{$rule_id}; } } } } delete $all_object_fabricators{$self}; } 1; =pod =head1 NAME UR::Context::ObjectFabricator - Track closures used to fabricate objects from data sources =head1 DESCRIPTION Object Fabricators are closures that accept listrefs of data returned by data source iterators, take slices out of them, and construct UR objects out of the results. They also handle updating the query cache and merging changed DB data with previously cached objects. UR::Context::ObjectFabricator objects are used internally by UR::Context, and not intended to be used directly. =head1 METHODS =over 4 =item create_for_loading_template my $fab = UR::Context::ObjectFabricator->create_for_loading_template( $context, $loading_tmpl_hashref, $template_data, $rule, $rule_template, $values, $dsx); Returns an object fabricator instance that is able to construct objects of the rule's target class from rows of data returned by data source iterators. Object fabricators are used a part of the object loading process, and are called by UR::Context::get_objects_for_class_and_rule() to transform a row of data returned by a data source iterator into a UR object. For each class involved in a get request, the system prepares a loading template that describes which columns of the data source data are to be used to construct an instance of that class. For example, in the case where a get() is done on a child class, and the parent and child classes store data in separate tables linked by a relation-property/foreign-key, then the query against the data source will involve and SQL join (for RDBMS data sources). That join will produce a result set that includes data from both tables. The C<$loading_tmpl_hashref> will have information about which columns of that result set map to which properties of each involved class. The heart of the fabricator closure is a list slice extracting the data for that class and assigning it to a hash slice of properties to fill in the initial object data for its class. The remainder of the closure is bookkeeping to keep the object cache ($UR::Context::all_objects_loaded) and query cache ($UR::Context::all_params_loaded) consistent. The interaction of the object fabricator, the query cache, object cache pruner and object loading iterators that may or may not have loaded all their data requires that the object fabricators keep a list of changes they plan to make to the query cache instead of applying them directly. When the Underlying Context Loading iterator has loaded the last row from the Data Source Iterator, it calls C on the object fabricator to tell it to go ahead and apply its changes; essentially treating that data as a transaction. =item all_object_fabricators my @fabs = UR::Context::ObjectFabricator->all_object_fabricators(); Returns a list of all object fabricators that have not yet been finalized =item fabricate my $ur_object = $fab->fabricate([columns,from,data,source]); Given a listref of data pulled from a data source iterator, it slices out the appropriate columns from the list and constructs a single object to return. =item is_loading_in_progress_for_boolexpr my $bool = $fab->is_loading_in_progress_for_boolexpr($boolexpr); Given a UR::BoolExpr instance, it returns true if the given fabricator is prepared to construct objects matching this boolexpr. This is used by UR::Context to know if other iterators are still pulling in objects that could match another iterator's boolexpr, and it should therefore not trust that the object cache is conplete. =item finalize $fab->finalize(); Indicates to the iterator that the caller is done using it for constructing objects, probably because the data source has no more data or the iterator that was using this fabricator has gone out of scope. =item apply_all_params_loaded $fab->apply_all_params_loaded(); As the fabricator constructs objects, it buffers changes to all_params_loaded (the Context's query cache) to maintain consistency if multiple iterators are working concurrently. At the appripriate time, call apply_all_params_loaded() to take those changes and apply them to the current Context's all_params_loaded. =back =cut ImportIterator.pm000444023532023421 12255512121654175 17736 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/Contextpackage UR::Context; # Methods related to the import iterator (part of the loading process). # # They are broken out here for readability purposes. The methods still live # in the UR::Context namespace. use strict; use warnings; our $VERSION = "0.41"; # UR $VERSION; # A wrapper around the method of the same name in UR::DataSource::* to iterate over the # possible data sources involved in a query. The easy case (a query against a single data source) # will return the $primary_template data structure. If the query involves more than one data source, # then this method also returns a list containing triples (@addl_loading_info) where each member is: # 1) The secondary data source name # 2) a listref of delegated properties joining the primary class to the secondary class # 3) a rule template applicable against the secondary data source sub _resolve_query_plan_for_ds_and_bxt { my($self,$primary_data_source,$rule_template) = @_; my $primary_query_plan = $primary_data_source->_resolve_query_plan($rule_template); unless ($primary_query_plan->{'joins_across_data_sources'}) { # Common, easy case return $primary_query_plan; } my @addl_loading_info; foreach my $secondary_data_source_id ( keys %{$primary_query_plan->{'joins_across_data_sources'}} ) { my $this_ds_delegations = $primary_query_plan->{'joins_across_data_sources'}->{$secondary_data_source_id}; my %seen_properties; foreach my $delegated_property ( @$this_ds_delegations ) { my $delegated_property_name = $delegated_property->property_name; next if ($seen_properties{$delegated_property_name}++); my $operator = $rule_template->operator_for($delegated_property_name); $operator ||= '='; # FIXME - shouldn't the template return this for us? my @secondary_params = ($delegated_property->to . ' ' . $operator); my $class_meta = UR::Object::Type->get($delegated_property->class_name); my $relation_property = $class_meta->property_meta_for_name($delegated_property->via); my $secondary_class = $relation_property->data_type; # we can also add in any properties in the property's joins that also appear in the rule my @property_pairs = $relation_property->get_property_name_pairs_for_join(); foreach my $pair ( @property_pairs ) { my($primary_property, $secondary_property) = @$pair; next if ($seen_properties{$primary_property}++); next unless ($rule_template->specifies_value_for($primary_property)); my $operator = $rule_template->operator_for($primary_property); $operator ||= '='; push @secondary_params, "$secondary_property $operator"; } my $secondary_rule_template = UR::BoolExpr::Template->resolve($secondary_class, @secondary_params); # FIXME there should be a way to collect all the requests for the same datasource together... # FIXME - currently in the process of switching to object-based instead of class-based data sources # For now, data sources are still singleton objects, so this get() will work. When we're fully on # regular-object-based data sources, then it'll probably change to UR::DataSource->get($secondary_data_source_id); my $secondary_data_source = UR::DataSource->get($secondary_data_source_id) || $secondary_data_source_id->get(); push @addl_loading_info, $secondary_data_source, [$delegated_property], $secondary_rule_template; } } return ($primary_query_plan, @addl_loading_info); } # Used by _create_secondary_loading_comparators to convert a rule against the primary data source # to a rule that can be used against a secondary data source # FIXME this might be made simpler be leaning on infer_property_value_from_rule()? sub _create_secondary_rule_from_primary { my($self,$primary_rule, $delegated_properties, $secondary_rule_template) = @_; my @secondary_values; my %seen_properties; # FIXME - we've already been over this list in _resolve_query_plan_for_ds_and_bxt()... # FIXME - is there ever a case where @$delegated_properties will be more than one item? foreach my $property ( @$delegated_properties ) { my $value = $primary_rule->value_for($property->property_name); my $secondary_property_name = $property->to; my $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name); $secondary_values[$pos] = $value; $seen_properties{$property->property_name}++; my $class_meta = $property->class_meta; my $via_property = $class_meta->property_meta_for_name($property->via); my @pairs = $via_property->get_property_name_pairs_for_join(); foreach my $pair ( @pairs ) { my($primary_property_name, $secondary_property_name) = @$pair; next if ($seen_properties{$primary_property_name}++); $value = $primary_rule->value_for($primary_property_name); next unless $value; $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name); $secondary_values[$pos] = $value; } } my $secondary_rule = $secondary_rule_template->get_rule_for_values(@secondary_values); return $secondary_rule; } # Since we'll be appending more "columns" of data to the listrefs returned by # the primary datasource's query, we need to apply fixups to the column positions # to all the secondary loading templates # The column_position and object_num offsets needed for the next call of this method # are returned sub _fixup_secondary_loading_template_column_positions { my($self,$primary_loading_templates, $secondary_loading_templates, $column_position_offset, $object_num_offset) = @_; if (! defined($column_position_offset) or ! defined($object_num_offset)) { $column_position_offset = 0; foreach my $tmpl ( @{$primary_loading_templates} ) { $column_position_offset += scalar(@{$tmpl->{'column_positions'}}); } $object_num_offset = scalar(@{$primary_loading_templates}); } my $this_template_column_count; foreach my $tmpl ( @$secondary_loading_templates ) { foreach ( @{$tmpl->{'column_positions'}} ) { $_ += $column_position_offset; } foreach ( @{$tmpl->{'id_column_positions'}} ) { $_ += $column_position_offset; } $tmpl->{'object_num'} += $object_num_offset; $this_template_column_count += scalar(@{$tmpl->{'column_positions'}}); } return ($column_position_offset + $this_template_column_count, $object_num_offset + scalar(@$secondary_loading_templates) ); } # For queries that have to hit multiple data sources, this method creates two lists of # closures. The first is a list of object fabricators, where the loading templates # have been given fixups to the column positions (see _fixup_secondary_loading_template_column_positions()) # The second is a list of closures for each data source (the @addl_loading_info stuff # from _resolve_query_plan_for_ds_and_bxt) that's able to compare the row loaded from the # primary data source and see if it joins to a row from this secondary datasource's database sub _create_secondary_loading_closures { my($self, $primary_template, $rule, @addl_loading_info) = @_; my $loading_templates = $primary_template->{'loading_templates'}; # Make a mapping of property name to column positions returned by the primary query my %primary_query_column_positions; foreach my $tmpl ( @$loading_templates ) { my $property_name_count = scalar(@{$tmpl->{'property_names'}}); for (my $i = 0; $i < $property_name_count; $i++) { my $property_name = $tmpl->{'property_names'}->[$i]; my $pos = $tmpl->{'column_positions'}->[$i]; $primary_query_column_positions{$property_name} = $pos; } } my @secondary_object_importers; my @addl_join_comparators; # used to shift the apparent column position of the secondary loading template info my ($column_position_offset,$object_num_offset); while (@addl_loading_info) { my $secondary_data_source = shift @addl_loading_info; my $this_ds_delegations = shift @addl_loading_info; my $secondary_rule_template = shift @addl_loading_info; my $secondary_rule = $self->_create_secondary_rule_from_primary ( $rule, $this_ds_delegations, $secondary_rule_template, ); $secondary_data_source = $secondary_data_source->resolve_data_sources_for_rule($secondary_rule); my $secondary_template = $self->_resolve_query_plan_for_ds_and_bxt($secondary_data_source,$secondary_rule_template); # sets of triples where the first in the triple is the column index in the # $secondary_db_row (in the join_comparator closure below), the second is the # index in the $next_db_row. And the last is a flag indicating if we should # perform a numeric comparison. This way we can preserve the order the comparisons # should be done in my @join_comparison_info; foreach my $property ( @$this_ds_delegations ) { # first, map column names in the joined class to column names in the primary class my %foreign_property_name_map; my @this_property_joins = $property->_resolve_join_chain(); foreach my $join ( @this_property_joins ) { my @source_names = @{$join->{'source_property_names'}}; my @foreign_names = @{$join->{'foreign_property_names'}}; @foreign_property_name_map{@foreign_names} = @source_names; } # Now, find out which numbered column in the result query maps to those names my $secondary_loading_templates = $secondary_template->{'loading_templates'}; foreach my $tmpl ( @$secondary_loading_templates ) { my $property_name_count = scalar(@{$tmpl->{'property_names'}}); for (my $i = 0; $i < $property_name_count; $i++) { my $property_name = $tmpl->{'property_names'}->[$i]; if ($foreign_property_name_map{$property_name}) { # This is the one we're interested in... Where does it come from in the primary query? my $column_position = $tmpl->{'column_positions'}->[$i]; # What are the types involved? my $primary_query_column_name = $foreign_property_name_map{$property_name}; my $primary_property_meta = UR::Object::Property->get(class_name => $primary_template->{'class_name'}, property_name => $primary_query_column_name); my $secondary_property_meta = UR::Object::Property->get(class_name => $secondary_template->{'class_name'}, property_name => $property_name); my $comparison_type; if ($primary_property_meta->is_numeric && $secondary_property_meta->is_numeric) { $comparison_type = 1; } my $comparison_position; if (exists $primary_query_column_positions{$primary_query_column_name} ) { $comparison_position = $primary_query_column_positions{$primary_query_column_name}; } else { # This isn't a real column we can get from the data source. Maybe it's # in the constant_property_names of the primary_loading_template? unless (grep { $_ eq $primary_query_column_name} @{$loading_templates->[0]->{'constant_property_names'}}) { die sprintf("Can't resolve datasource comparison to join %s::%s to %s:%s", $primary_template->{'class_name'}, $primary_query_column_name, $secondary_template->{'class_name'}, $property_name); } my $comparison_value = $rule->value_for($primary_query_column_name); unless (defined $comparison_value) { $comparison_value = $self->infer_property_value_from_rule($primary_query_column_name, $rule); } $comparison_position = \$comparison_value; } push @join_comparison_info, $column_position, $comparison_position, $comparison_type; } } } } my $secondary_db_iterator = $secondary_data_source->create_iterator_closure_for_rule($secondary_rule); my $secondary_db_row; # For this closure, pass in the row we just loaded from the primary DB query. # This one will return the data from this secondary DB's row if the passed-in # row successfully joins to this secondary db iterator. It returns an empty list # if there were no matches, and returns false if there is no more data from the query my $join_comparator = sub { my $next_db_row = shift; # From the primary DB READ_DB_ROW: while(1) { return unless ($secondary_db_iterator); unless ($secondary_db_row) { ($secondary_db_row) = $secondary_db_iterator->(); unless($secondary_db_row) { # No more data to load $secondary_db_iterator = undef; return; } } for (my $i = 0; $i < @join_comparison_info; $i += 3) { my $secondary_column = $join_comparison_info[$i]; my $primary_column = $join_comparison_info[$i+1]; my $is_numeric = $join_comparison_info[$i+2]; my $comparison; if (ref $primary_column) { # This was one of those constant value items if ($is_numeric) { $comparison = $secondary_db_row->[$secondary_column] <=> $$primary_column; } else { $comparison = $secondary_db_row->[$secondary_column] cmp $$primary_column; } } else { if ($join_comparison_info[$i+2]) { $comparison = $secondary_db_row->[$secondary_column] <=> $next_db_row->[$primary_column]; } else { $comparison = $secondary_db_row->[$secondary_column] cmp $next_db_row->[$primary_column]; } } if ($comparison < 0) { # less than, get the next row from the secondary DB $secondary_db_row = undef; redo READ_DB_ROW; } elsif ($comparison == 0) { # This one was the same, keep looking at the others } else { # greater-than, there's no match for this primary DB row return 0; } } # All the joined columns compared equal, return the data return $secondary_db_row; } }; Sub::Name::subname('UR::Context::__join_comparator(closure)__', $join_comparator); push @addl_join_comparators, $join_comparator; # And for the object importer/fabricator, here's where we need to shift the column order numbers # over, because these closures will be called after all the db iterators' rows are concatenated # together. We also need to make a copy of the loading_templates list so as to not mess up the # class' notion of where the columns are # FIXME - it seems wasteful that we need to re-created this each time. Look into some way of using # the original copy that lives in $primary_template->{'loading_templates'}? Somewhere else? my @secondary_loading_templates; foreach my $tmpl ( @{$secondary_template->{'loading_templates'}} ) { my %copy; foreach my $key ( keys %$tmpl ) { my $value_to_copy = $tmpl->{$key}; if (ref($value_to_copy) eq 'ARRAY') { $copy{$key} = [ @$value_to_copy ]; } elsif (ref($value_to_copy) eq 'HASH') { $copy{$key} = { %$value_to_copy }; } else { $copy{$key} = $value_to_copy; } } push @secondary_loading_templates, \%copy; } ($column_position_offset,$object_num_offset) = $self->_fixup_secondary_loading_template_column_positions($primary_template->{'loading_templates'}, \@secondary_loading_templates, $column_position_offset,$object_num_offset); #my($secondary_rule_template,@secondary_values) = $secondary_rule->get_template_and_values(); my @secondary_values = $secondary_rule->values(); foreach my $secondary_loading_template ( @secondary_loading_templates ) { my $secondary_object_importer = UR::Context::ObjectFabricator->create_for_loading_template( $self, $secondary_loading_template, $secondary_template, $secondary_rule, $secondary_rule_template, \@secondary_values, $secondary_data_source ); next unless $secondary_object_importer; push @secondary_object_importers, $secondary_object_importer; } } return (\@secondary_object_importers, \@addl_join_comparators); } # This returns an iterator that is used to bring objects in from an underlying # context into this context. sub _create_import_iterator_for_underlying_context { my ($self, $rule, $dsx, $this_get_serial) = @_; # TODO: instead of taking a data source, resolve this internally. # The underlying context itself should be responsible for its data sources. # Make an iterator for the primary data source. # Primary here meaning the one for the class we're explicitly requesting. # We may need to join to other data sources to complete the query. my ($db_iterator) = $dsx->create_iterator_closure_for_rule($rule); my ($rule_template, @values) = $rule->template_and_values(); my ($query_plan,@addl_loading_info) = $self->_resolve_query_plan_for_ds_and_bxt($dsx,$rule_template); my $class_name = $query_plan->{class_name}; my $group_by = $rule_template->group_by; my $order_by = $rule_template->order_by; my $aggregate = $rule_template->aggregate; my $limit = $rule_template->limit; if (my $sub_typing_property) { # When the rule has a property specified which indicates a specific sub-type, catch this and re-call # this method recursively with the specific subclass name. my ($rule_template, @values) = $rule->template_and_values(); my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype}; my $class_table_name = $query_plan->{class_table_name}; warn "Implement me carefully"; if ($rule_template_specifies_value_for_subtype) { my $sub_classification_meta_class_name = $query_plan->{sub_classification_meta_class_name}; my $value = $rule->value_for($sub_typing_property); my $type_obj = $sub_classification_meta_class_name->get($value); if ($type_obj) { my $subclass_name = $type_obj->subclass_name($class_name); if ($subclass_name and $subclass_name ne $class_name) { #$rule = $subclass_name->define_boolexpr($rule->params_list, $sub_typing_property => $value); $rule = UR::BoolExpr->resolve_normalized($subclass_name, $rule->params_list, $sub_typing_property => $value); return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial); } } else { die "No $value for $class_name?\n"; } } elsif (not $class_table_name) { die "No longer supported!"; my $rule = UR::BoolExpr->resolve( $class_name, $rule_template->get_rule_for_values(@values)->params_list, ); return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial) } else { # continue normally # the logic below will handle sub-classifying each returned entity } } my $loading_templates = $query_plan->{loading_templates}; my $sub_typing_property = $query_plan->{sub_typing_property}; my $next_db_row; my $rows = 0; # number of rows the query returned my $recursion_desc = $query_plan->{recursion_desc}; my($rule_template_without_recursion_desc, $rule_template_id_without_recursion); my($rule_without_recursion_desc, $rule_id_without_recursion); # These get set if you're doing a -recurse query, and the underlying data source doesn't support recursion my($by_hand_recursive_rule_template,$by_hand_recursive_source_property,@by_hand_recursive_source_values,$by_hand_recursing_iterator); if ($recursion_desc) { $rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc}; $rule_template_id_without_recursion = $rule_template_without_recursion_desc->id; $rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values); $rule_id_without_recursion = $rule_without_recursion_desc->id; if ($query_plan->{'recurse_resolution_by_iteration'}) { # The data source does not support a recursive query. Accomplish the same thing by # recursing back into _create_import_iterator_for_underlying_context for each level my $this; ($this,$by_hand_recursive_source_property) = @$recursion_desc; my @extra; $by_hand_recursive_rule_template = UR::BoolExpr::Template->resolve($class_name, "$this in"); $by_hand_recursive_rule_template->recursion_desc($recursion_desc); if (!$by_hand_recursive_rule_template or @extra) { Carp::croak("Can't resolve recursive query: Class $class_name cannot filter by one or more properties: " . join(', ', @extra)); } } } my $rule_id = $rule->id; my $rule_template_id = $rule_template->id; my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'}; my %subordinate_iterator_for_class; # TODO: move the creation of the fabricators into the query plan object initializer. # instead of making just one import iterator, we make one per loading template # we then have our primary iterator use these to fabricate objects for each db row my @object_fabricators; if ($group_by) { # returning sets for each sub-group instead of instance objects... my $division_point = scalar(@$group_by)-1; my $subset_template = $rule_template->_template_for_grouped_subsets(); my $set_class = $class_name . '::Set'; my @aggregate_properties = ($aggregate ? @$aggregate : ()); unshift(@aggregate_properties, 'count') unless (grep { $_ eq 'count' } @aggregate_properties); my $fab_subref = sub { my $row = $_[0]; my @group_values = @$row[0..$division_point]; my $ss_rule = $subset_template->get_rule_for_values(@values, @group_values); my $set = $set_class->get($ss_rule->id); unless ($set) { Carp::croak("Failed to fabricate $set_class for rule $ss_rule"); } @$set{@aggregate_properties} = @$row[$division_point+1..$#$row]; return $set; }; my $object_fabricator = UR::Context::ObjectFabricator->_create( fabricator => $fab_subref, context => $self, ); unshift @object_fabricators, $object_fabricator; } else { # regular instances for my $loading_template (@$loading_templates) { my $object_fabricator = UR::Context::ObjectFabricator->create_for_loading_template( $self, $loading_template, $query_plan, $rule, $rule_template, \@values, $dsx, ); next unless $object_fabricator; unshift @object_fabricators, $object_fabricator; } } # For joins across data sources, we need to create importers/fabricators for those # classes, as well as callbacks used to perform the equivalent of an SQL join in # UR-space my @addl_join_comparators; if (@addl_loading_info) { if ($group_by) { Carp::croak("cross-datasource group-by is not supported yet"); } my($addl_object_fabricators, $addl_join_comparators) = $self->_create_secondary_loading_closures( $query_plan, $rule, @addl_loading_info ); unshift @object_fabricators, @$addl_object_fabricators; push @addl_join_comparators, @$addl_join_comparators; } # To avoid calling the useless method 'fabricate' on a fabricator object for each object of each resultset row my @object_fabricator_closures = map { $_->fabricator } @object_fabricators; # Insert the key into all_objects_are_loaded to indicate that when we're done loading, we'll # have everything if ($query_plan->{'rule_matches_all'} and not $group_by) { $class_name->all_objects_are_loaded(undef); } #my $is_monitor_query = $self->monitor_query(); # Make the iterator we'll return. my $next_object_to_return; my @object_ids_from_fabricators; my $underlying_context_iterator = sub { return undef unless $db_iterator; my $primary_object_for_next_db_row; LOAD_AN_OBJECT: until (defined $primary_object_for_next_db_row) { # note that we return directly when the db is out of data my ($next_db_row); ($next_db_row) = $db_iterator->() if ($db_iterator); if (! $next_db_row and $by_hand_recursive_rule_template and @by_hand_recursive_source_values) { # DB is out of results for this query, we need to handle recursion here in the context # and there are values to recurse on unless ($by_hand_recursing_iterator) { # Do a new get() on the data source to recursively get more data my $recurse_rule = $by_hand_recursive_rule_template->get_rule_for_values(\@by_hand_recursive_source_values); $by_hand_recursing_iterator = $self->_create_import_iterator_for_underlying_context($recurse_rule,$dsx,$this_get_serial); } my $retval = $next_object_to_return; $next_object_to_return = $by_hand_recursing_iterator->(); unless ($next_object_to_return) { $by_hand_recursing_iterator = undef; $by_hand_recursive_rule_template = undef; } return $retval; } unless ($next_db_row) { $db_iterator = undef; if ($rows == 0) { # if we got no data at all from the sql then we give a status # message about it and we update all_params_loaded to indicate # that this set of parameters yielded 0 objects my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only}; if ($rule_template_is_id_only) { my $id = $rule->value_for_id; $UR::Context::all_objects_loaded->{$class_name}->{$id} = undef; } else { $UR::Context::all_params_loaded->{$rule_template_id}->{$rule_id} = 0; } } if ( $query_plan->{rule_matches_all} ) { # No parameters. We loaded the whole class. # Doing a load w/o a specific ID w/o custom SQL loads the whole class. # Set a flag so that certain optimizations can be made, such as # short-circuiting future loads of this class. # # If the key still exists in the all_objects_are_loaded hash, then # we can set it to true. This is needed in the case where the user # gets an iterator for all the objects of some class, but unloads # one or more of the instances (be calling unload or through the # cache pruner) before the iterator completes. If so, _abandon_object() # will have removed the key from the hash if (exists($UR::Context::all_objects_are_loaded->{$class_name})) { $class_name->all_objects_are_loaded(1); } } if ($recursion_desc) { my @results = $class_name->is_loaded($rule_without_recursion_desc); $UR::Context::all_params_loaded->{$rule_template_id_without_recursion}{$rule_id_without_recursion} = scalar(@results); for my $object (@results) { $object->{__load}->{$rule_template_id_without_recursion}->{$rule_id_without_recursion}++; } } # Apply changes to all_params_loaded that each importer has collected foreach (@object_fabricators) { $_->finalize if $_; } # If the SQL for the subclassed items was constructed properly, then each # of these iterators should be at the end, too. Call them one more time # so they'll finalize their object fabricators. foreach my $class ( keys %subordinate_iterator_for_class ) { my $obj = $subordinate_iterator_for_class{$class}->(); if ($obj) { # The last time this happened, it was because a get() was done on an abstract # base class with only 'id' as a param. When the subclassified rule was # turned into SQL in UR::DataSource::QueryPlan() # it removed that one 'id' filter, since it assummed any class with more than # one ID property (usually classes have a named whatever_id property, and an alias 'id' # property) will have a rule that covered both ID properties Carp::carp("Leftover objects in subordinate iterator for $class. This shouldn't happen, but it's not fatal..."); while ($obj = $subordinate_iterator_for_class{$class}->()) {1;} } } my $retval = $next_object_to_return; $next_object_to_return = undef; return $retval; } # we count rows processed mainly for more concise sanity checking $rows++; # For multi-datasource queries, does this row successfully join with all the other datasources? # # Normally, the policy is for the data source query to return (possibly) more than what you # asked for, and then we'd cache everything that may have been loaded. In this case, we're # making the choice not to. Reason being that a join across databases is likely to involve # a lot of objects, and we don't want to be stuffing our object cache with a lot of things # we're not interested in. FIXME - in order for this to be true, then we could never query # these secondary data sources against, say, a calculated property because we're never turning # them into objects. FIXME - fix this by setting the $needs_further_boolexpr_evaluation_after_loading # flag maybe? my @secondary_data; foreach my $callback (@addl_join_comparators) { # FIXME - (no, not another one...) There's no mechanism for duplicating SQL join's # behavior where if a row from a table joins to 2 rows in the secondary table, the # first table's data will be in the result set twice. my $secondary_db_row = $callback->($next_db_row); unless (defined $secondary_db_row) { # That data source has no more data, so there can be no more joins even if the # primary data source has more data left to read $db_iterator = undef; $primary_object_for_next_db_row = undef; last LOAD_AN_OBJECT; } unless ($secondary_db_row) { # It returned 0 # didn't join (but there is still more data we can read later)... throw this row out. $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } # $next_db_row is a read-only value from DBI, so we need to track our additional # data seperately and smash them together before the object importer is called push(@secondary_data, @$secondary_db_row); } # get one or more objects from this row of results my $re_iterate = 0; my @imported; for (my $i = 0; $i < @object_fabricator_closures; $i++) { my $object_fabricator = $object_fabricator_closures[$i]; # The usual case is that the query is just against one data source, and so the importer # callback is just given the row returned from the DB query. For multiple data sources, # we need to smash together the primary and all the secondary lists my $imported_object; #my $object_creation_time; #if ($is_monitor_query) { # $object_creation_time = Time::HiRes::time(); #} if (@secondary_data) { $imported_object = $object_fabricator->([@$next_db_row, @secondary_data]); } else { $imported_object = $object_fabricator->($next_db_row); } #if ($is_monitor_query) { # $self->_log_query_for_rule($class_name, $rule, sprintf("QUERY: object fabricator took %.4f s",Time::HiRes::time() - $object_creation_time)); #} if ($imported_object and not ref($imported_object)) { # object requires sub-classsification in a way which involves different db data. $re_iterate = 1; } push @imported, $imported_object; # If the object ID for fabricator slot $i changes, then we can apply the # all_params_loaded changes from iterators 0 .. $i-1 because we know we've # loaded all the hangoff data related to the previous object # remember that the last fabricator in the list is for the primary object if (defined $imported_object and ref($imported_object)) { if (!defined $object_ids_from_fabricators[$i]) { $object_ids_from_fabricators[$i] = $imported_object->id; } elsif ($object_ids_from_fabricators[$i] ne $imported_object->id) { for (my $j = 0; $j < $i; $j++) { $object_fabricators[$j]->apply_all_params_loaded; } $object_ids_from_fabricators[$i] = $imported_object->id; } } } $primary_object_for_next_db_row = $imported[-1]; # The object importer will return undef for an object if no object # got created for that $next_db_row, and will return a string if the object # needs to be subclassed before being returned. Don't put serial numbers on # these map { $_->{'__get_serial'} = $this_get_serial } grep { defined && ref } @imported; if ($re_iterate and defined($primary_object_for_next_db_row) and ! ref($primary_object_for_next_db_row)) { # It is possible that one or more objects go into subclasses which require more # data than is on the results row. For each subclass (or set of subclasses), # we make a more specific, subordinate iterator to delegate-to. my $subclass_name = $primary_object_for_next_db_row; my $subclass_meta = UR::Object::Type->get(class_name => $subclass_name); my $table_subclass = $subclass_meta->most_specific_subclass_with_table(); my $sub_iterator = $subordinate_iterator_for_class{$table_subclass}; unless ($sub_iterator) { #print "parallel iteration for loading $subclass_name under $class_name!\n"; my $sub_classified_rule_template = $rule_template->sub_classify($subclass_name); my $sub_classified_rule = $sub_classified_rule_template->get_normalized_rule_for_values(@values); $sub_iterator = $subordinate_iterator_for_class{$table_subclass} = $self->_create_import_iterator_for_underlying_context($sub_classified_rule,$dsx,$this_get_serial); } ($primary_object_for_next_db_row) = $sub_iterator->(); if (! defined $primary_object_for_next_db_row) { # the newly subclassed object redo LOAD_AN_OBJECT; } } # end of handling a possible subordinate iterator delegate unless (defined $primary_object_for_next_db_row) { #if (!$primary_object_for_next_db_row or $rule->evaluate($primary_object_for_next_db_row)) { redo LOAD_AN_OBJECT; } if ( !$group_by and (ref($primary_object_for_next_db_row) ne $class_name) and (not $primary_object_for_next_db_row->isa($class_name)) ) { $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } if ($by_hand_recursive_source_property) { my @values = grep { defined } $primary_object_for_next_db_row->$by_hand_recursive_source_property; push @by_hand_recursive_source_values, @values; } if (! defined($next_object_to_return) or (Scalar::Util::refaddr($next_object_to_return) == Scalar::Util::refaddr($primary_object_for_next_db_row)) ) { # The first time through the iterator, we need to buffer the object until # $primary_object_for_next_db_row is something different. $next_object_to_return = $primary_object_for_next_db_row; $primary_object_for_next_db_row = undef; redo LOAD_AN_OBJECT; } } # end of loop until we have a defined object to return #foreach my $object_fabricator ( @object_fabricators ) { # # Don't apply all_params_loaded for primary fab until it's all done # next if ($object_fabricator eq $object_fabricators[-1]); # $object_fabricator->apply_all_params_loaded; #} my $retval = $next_object_to_return; $next_object_to_return = $primary_object_for_next_db_row; return $retval; }; Sub::Name::subname('UR::Context::__underlying_context_iterator(closure)__', $underlying_context_iterator); return $underlying_context_iterator; } 1; DBI000755023532023421 012121654174 13156 5ustar00abrummetgsc000000000000UR-0.41/lib/URReport.pm000444023532023421 4517012121654174 15153 0ustar00abrummetgsc000000000000UR-0.41/lib/UR/DBI =pod =head1 NAME UR::DBI::Report - a database report interface =head1 SYNOPSIS ##- use UR::DBI::Report; UR::DBI::Report->use_standard_cmdline_options(); UR::DBI::Report->generate(sql => \@ARGV); =head1 DESCRIPTION This module is a reporting interface which takes SQL queries in a variety of forms and prints their results with formatting options. =cut use strict; use warnings; package UR::DBI::Report; use base 'UR::ModuleBase'; require UR; our $VERSION = "0.41"; # UR $VERSION; use Data::Dumper; use Time::HiRes; # Support some options right on the "use" line. sub import { my $class = shift; my %params = @_; UR::DBI::Report->extend_command_line() if delete $params{extend_command_line}; die "Unknown options passed-to " . __PACKAGE__ . join(", ", keys %params) if keys %params; } # Applications which do no additional configuration will get these parameters by default. our %module_default_params = ( delimit => 'spaces', header => 1, count => 1, orient => 'vert', trunc => 35, sloppy => 0, nulls => 1, data => 1, combine => 0, "explain-sql" => 0, ); # Applications which call this method before init() will allow the user # to override reporting defaults via standard command line options. our %application_default_params = %module_default_params; sub extend_command_line { # this callback processes all of the options and sets application defaults for this module my $parse_option_callback; $parse_option_callback = sub { my ($flag,$value) = @_; if ($flag eq 'parse') { $parse_option_callback->("header",!$value); $parse_option_callback->("count",!$value); $parse_option_callback->("delimit",($value ? "tabs" : "spaces")); $parse_option_callback->("trunc",($value ? undef : $application_default_params{"trunc"})); return 1; } $application_default_params{$flag} = $value; return 1; }; # ask Getopt to expect some new cmdline parameters UR::Command::Param->add( map { if (ref($_)) { $_->{module} = "Data Report Formatting" } $_; } delimit => { action => $parse_option_callback, msg => "spaces|tabs: spaces separate columns evenly, tab-delimited columns are easiliy parsed", argument => '=s', option => '--delimit', }, header => { action => $parse_option_callback, msg => "Show column headers.", argument => '!', option => '--header', }, data => { action => $parse_option_callback, msg => "Show returned query data (on by default!).", argument => '!', option => '--data', }, count => { action => $parse_option_callback, msg => "Show row count at the end of output.", argument => '!', option => '--count', }, orient => { action => $parse_option_callback, msg => "vert: (default) one row per output line, horiz: one row per output column.", argument => '=s', option => '--orient', }, trunc => { action => $parse_option_callback, msg => "Set column truncation for long values. A zero setting truncates at the level of the default DBI LongReadLen; see DBI documentation for details.", argument => '=s', option => '--trunc', }, sloppy => { action => $parse_option_callback, msg => "When processing multiple SQL statements and a failure occurs, just proceed to the next statement.", argument => '!', option => '--sloppy', }, nulls => { action => $parse_option_callback, msg => "Show nulls. When turned-off with 'no-nulls' replaces them with a ?.", argument => '!', option => '--nulls', }, parse => { action => $parse_option_callback, msg => "Equivalent to --noheader --nocount --tabs --trunc=0", argument => '!', option => '--parse', }, echo => { action => $parse_option_callback, msg => "Print SQL before its first execution. Does not print multiple times on multiple executes with different params.", argument => '!', option => '--echo', }, combine => { action => $parse_option_callback, msg => "When executing the same query multiple times with different params, combine the results as though it were one query.", argument => '!', option => '--combine', }, "explain-sql" => { action => $parse_option_callback, msg => "Dump a query plan instead of running the query.", argument => '!', option => '--explain-sql', } ); } # # This method executes the specified sql statements and prints reports for each. # sub generate { my $class = shift; my %params = (%application_default_params, @_); my $sql_param = delete $params{sql}; my @queries = (ref($sql_param) ? (@$sql_param) : ($sql_param) ); my $dbh = delete $params{dbh}; unless ($dbh) { Carp::confess("No dbh sent to UR::DBI::Report, and no default available anymore!"); } $dbh->{LongTruncOk} = 1; if ($params{trunc}) { $dbh->{LongReadLen} = $params{trunc}; } elsif(defined($params{trunc})) { warn "Setting the trunc value to 0 does not guarantee no truncating."; warn "There is no way to completely prevent truncating in the current version of DBI."; warn "The current trunc limit is $dbh->{LongReadLen}."; warn "If this does not satisfy your needs, try setting trunc to a higher number"; } # The outer loop runs once per SQL statement. my $sql_request; while($sql_request = shift(@queries)) { # The SQL comes from the cmdline or STDIN. my $sql; if ($sql_request eq '-') { $sql = ''; while () { next if (/^#!/); if (/;\s*$/) { s/;\s*$//; $sql .= $_; last; } else { $sql .= $_; } } } else { $sql = $sql_request; } next if ($sql !~ /\S/); chomp($sql); print "SQLRUN: $sql\n" if $params{echo}; # See if we expect paramters from STDIN my $question_marks = $sql; $question_marks =~ s/[^\?]//msg; my $question_mark_count = length($question_marks); if ($params{"explain-sql"}) { my $outfh = IO::Handle->new; $outfh->fdopen(fileno(STDOUT), 'w'); UR::DBI::_print_query_plan($sql,$dbh,outfh => $outfh,%params); # skip past any parameters, since we're not really executing, # and they don't (can't) affect the query plan if ($question_mark_count) { my $data; while (1) { $data = ; chomp $data; last unless (defined($data) and length($data)); } } # redo if we're reading from stdin, otherwise go to the next specified cmd if ($sql_request eq '-') { redo; } else { next; } } # This will never get re-prepared my $sth = $dbh->prepare_cached($sql); unless($sth) { if ($params{sloppy}) { App::UI->error_message($dbh->errstr); next; } else { die $dbh->errstr; } } # This flag may be set after the first parameter set runs to speed further executions. # The inner loop runs once per required execution of the SQL. # SQL is executed multiple times if there are ? placeholders and there are multiple lines on STDIN my ($combine_row_count, $combine_time)=(0, 0); my $sql_execution_count = 0; my $statement_is_not_a_query = 0; my $outfh = $params{outfh}; for (1) { # Get params from STDIN if necessary my @params; if ($question_mark_count) { # Get data from STDIN as needed for any ?s. my $data = ; chomp $data if defined($data); # If we have a ? count and there is no data on this line, we're done with this SQL statement. unless (defined($data) and length($data)) { # We want to warn the user if a SQL statement had no params at all. if ($sql_execution_count == 0) { $class->error_message("No params!"); } # On to the next staement, if there is one. last; } @params = split(/\t/, $data); $#params = $question_mark_count - 1; if ($params{echo}) { print "PARAMS: @params\n"; } } # Note the time so we can show the elapsed time. my $t1 = Time::HiRes::time(); # Execute the current statement with the parameters. my $execcnt; unless ($execcnt = $sth->execute(@params)) { my $msg = "Failed to execute SQL:\n$sql\n" . (@params ? "Data:\n>" . join(",",@params) . "<\n" : '') . $sth->errstr; if ($params{sloppy}) { App::UI->error_message($msg); } else { die $msg; } } # Count these for better error messaging. $sql_execution_count++; # Count results returned (SQL) or affected (DML). my $rowcnt; # This flag may not be set until we try to get the first result. unless ($statement_is_not_a_query) { $rowcnt = UR::DBI::Report->print_formatted( sth => $sth, outfh => $outfh, ( $params{combine} ? (position_in_combined_sql_list => $sql_execution_count) :() ), %params ); $statement_is_not_a_query = 1 if defined($rowcnt) and ($rowcnt eq "0 because the statement is not a query"); } # Flush any data pending to the output filter. if (ref($outfh) and not $params{combine} and not $params{outfh}) { $outfh->close; $outfh = undef; } $sth->finish; # Summarize the effect of the query/dml. if ($params{count}) { if($params{combine}) { #If we're doing a combined output, we'll have to tally these up for later $combine_row_count+=$statement_is_not_a_query?($execcnt+0):($rowcnt+0); $combine_time+=Time::HiRes::time()-$t1; } else { my $td = Time::HiRes::time() - $t1; $td =~ s/(\.\d\d\d).*/$1/; if ($statement_is_not_a_query) { print (($execcnt+0) . " row(s) affected. Execution time: ${td} second(s).\n"); } else { print (($rowcnt+0) . " row(s) returned. Execution time: ${td} second(s).\n"); } } } # By default this block will execute just once. # Continue if there is a question_mark_count. # It will "last" out at the top if there is no more data on stdin. redo if $question_mark_count; } # end params loop if ($params{combine}) { $outfh->close if ref($outfh) and not $params{outfh}; if ($params{count}) { $combine_time=~s/(\.\d\d\d).*/$1/; print("$combine_row_count row(s) ".($statement_is_not_a_query?'affected':'returned'). ". Execution time: $combine_time second(s).\n"); } } # If the cmdline sql was a dash, we're reading from STDIN until it exits the loop. redo if $sql_request eq '-'; } # end SQL loop # Done executing all SQL. return 1; } # end of sqlrun subroutine # This method prints a single report for a given statement handle. sub print_formatted { my $class = shift; my %params = (%application_default_params, @_); # sth A statement handle from which the data comes. # sql If no handle is specified, the SQL to use. # infh If no sth or sql is specified, a handle from which sql can be pulled. # If sth or sql ARE specifed, a handle from which parameter values can be pulled. my $sth = delete $params{sth}; unless ($sth) { } # outfh An optional handle to which the report is written. my $outfh = delete $params{outfh}; if ($outfh) { if ($params{delimit} =~ /^s/i && $^O ne "MSWin32" && $^O ne 'cygwin') { # We only handle one case of $outfh and still do tab2col. # If it's stderr, we redirect there. $outfh = IO::File->new('| tab2col --nocount 1>&' . fileno($outfh)); Carp::confess("Failed to pipe through tab2col!") unless $outfh; } } else { if ($params{delimit} =~ /^s/i) { # Handle tab-delimit via tab2col $outfh = IO::File->new("| tab2col --nocount"); } else { $outfh = IO::Handle->new; $outfh->fdopen(fileno(STDOUT), 'w'); } } # This is the return value. # Set to an integer, or to the false-valued string "0 because the statement is not a query". my $rowcnt = 0; # Get the column names into an array of headers. my @headers = @{ $sth->{NAME_uc} }; # Display as needed according the requested orientation. if ($params{orient} =~ /^v/i) # lines listed vertically { # Get the first row, but re-hook warnings first to see if we # are really running a query wich can return data (not DML). my $msg; local $SIG{__WARN__} = sub { $msg = shift }; my $row = $sth->fetchrow_arrayref; if ($msg =~ /ERROR no statement executing/) { # Set this flag so we do not re-try fetch*() on this query. return "0 because the statement is not a query"; } elsif ($sth->errstr) { die $sth->errstr; } else { if ($params{data}) { # Spacers are dashes. my @spacers = @headers; for (@spacers) { $_ =~ s/./-/g } # Print the headers, a line of spacers, then one line for each result row. if ($params{header} and not ($params{combine} and $params{position_in_combined_sql_list} > 1)) { if (my $trunc = $params{trunc}) { for my $row (\@headers, \@spacers) { print $outfh join("\t", map { substr($_,0,$trunc) } @$row) . "\n"; } } else { for my $row (\@headers, \@spacers) { print $outfh join("\t",@$row) . "\n"; } } } # Print the initial row, and any others we can fetch(). while ($row) { print $outfh join("\t",@$row) . "\n"; $rowcnt++; $row = $sth->fetchrow_arrayref; } } else { # Just get the count while ($row) { $rowcnt++; $row = $sth->fetchrow_arrayref } } } } elsif ($params{orient} =~ /^h/i) { my $msg; local $SIG{__WARN__} = sub { $msg = shift }; my $results = $sth->fetchall_arrayref; if ($msg =~ /ERROR no statement executing/) { # Set this flag so we do not re-try fetch*() on this query. return "0 because the statement is not a query"; } else { # Process the fetched data. $rowcnt = scalar(@$results); if ($params{data}) { # Show the data my $cnum = 0; if (my $trunc = $params{trunc}) { for my $header (@headers) { print $outfh $header . "\t:\t" if ($params{header}); print $outfh join("\t", map { substr($_->[$cnum],0,$trunc) } @$results) . "\n"; $cnum++; } } else { for my $header (@headers) { $outfh->print($header . "\t:\t") if ($params{header}); $outfh->print(join("\t", map { $_->[$cnum] } @$results) . "\n"); $cnum++; } } } } } else { $class->error_message("Unknown orientation $params{orient}"); return; } return $rowcnt; } 1; Devel000755023532023421 012121654173 13270 5ustar00abrummetgsc000000000000UR-0.41/libcallcount.pm000444023532023421 516212121654173 15753 0ustar00abrummetgsc000000000000UR-0.41/lib/Devel package Devel::callsfrom; use Data::Dumper; # From perldoc perlvar # Debugger flags, so you can see what we turn on below. # # 0x01 Debug subroutine enter/exit. # # 0x02 Line-by-line debugging. # # 0x04 Switch off optimizations. # # 0x08 Preserve more data for future interactive inspections. # # 0x10 Keep info about source lines on which a subroutine is defined. # # 0x20 Start with single-step on. # # 0x40 Use subroutine address instead of name when reporting. # # 0x80 Report "goto &subroutine" as well. # # 0x100 Provide informative "file" names for evals based on the place they were com- # piled. # # 0x200 Provide informative names to anonymous subroutines based on the place they # were compiled. # # 0x400 Debug assertion subroutines enter/exit. # BEGIN { $^P |= (0x01 | 0x80 | 0x100 | 0x200); }; #BEGIN { $^P |= (0x004 | 0x100 ); }; sub import { } package DB; # Any debugger needs to have a sub DB. It doesn't need to do anything. sub DB{}; # We want to track how deep our subroutines go our $CALL_DEPTH = 0; our %CALLED; our $CALL_WATCH = $ENV{CALL_WATCH}; sub sub { local $DB::CALL_DEPTH = $DB::CALL_DEPTH+1; no strict; no warnings; my @c0 = caller(0); my @c1 = caller(-1); my ($pkg,$file,$line) = @c1; my $csub = $c0[3] || '-'; my $caller = join(",", $file,$line,$pkg,$csub); print STDERR ((' ' x $DB::CALL_DEPTH) . $DB::sub{$DB::sub} . ' > ' . $DB::sub . "(@_) : " . $caller . "\n") if $CALL_WATCH; $DB::CALLED{$DB::sub}{$caller}++; &{$DB::sub}; } END { use strict; use warnings; my %counts; for my $sub (keys %DB::sub) { my $cases = $DB::CALLED{$sub}; my @callers = keys %$cases; my $call_count = scalar(@callers); $counts{$call_count}{$sub} = $cases; } my @counts = keys %counts; my $call_min = $ENV{CALL_MIN}; if (defined $call_min) { @counts = grep { $_ >= $call_min } @counts; } my $call_max = $ENV{CALL_MAX}; if (defined $call_max) { @counts = grep { $_ <= $call_max } @counts; } my $fh; if (my $fname = $ENV{CALL_COUNT_OUTFILE}) { open($fh,">$fname"); unless ($fh) { die "failed to open outfile for call count for $0!" }; } else { open($fh,">$0.callcount"); $fh or die "failed to open output file $0.callcount: $!"; } for my $c (sort { $a <=> $b } @counts) { my $subs = $counts{$c}; for my $sub (sort keys %$subs) { my $cases = $subs->{$sub}; my @calls = sort keys %$cases; print $fh join("\t",$c, $sub,$DB::sub{$sub},@calls),"\n"; } } } 1; t000755023532023421 012121654175 11730 5ustar00abrummetgsc000000000000UR-0.41above.t000444023532023421 242412121654172 13345 0ustar00abrummetgsc000000000000UR-0.41/t#!/usr/bin/env perl use strict; use warnings; use File::Temp; use Test::More tests => 4; use IO::File; BEGIN { $ENV{'PERL_ABOVE_QUIET'} = 1 if $ENV{'HARNESS_ACTIVE'}; } my $d = File::Temp::tempdir(CLEANUP => 1); ok($d, "created working directory $d"); mkdir "$d/lib1" or die $!; mkdir "$d/lib2" or die $!; IO::File->new(">$d/lib1/Foo.pm")->print('package Foo; 1'); IO::File->new(">$d/lib2/Foo.pm")->print('package Foo; 1'); mkdir "$d/lib1/Foo"; mkdir "$d/lib2/Foo"; eval 'use lib "$d/lib1/"'; chdir "$d/lib2/Foo"; eval "use above 'Foo'"; is(&clean_darwin($INC{"Foo.pm"}), "$d/lib2/Foo.pm", "used the expected module"); chdir "$d/" or die "Failed to chdir to $d/: $!"; my $src = $^X . q| -e 'use above "Foo"; print $INC{"Foo.pm"}'|; my $v = `$src`; is(&clean_darwin($v), "$d/lib2/Foo.pm", "Got the original module, not the 2nd one, and not an error."); chdir "$d/lib1" or die "Failed to chdir to $d/lib1: $!"; $v = `$src`; is(&clean_darwin($v), "$d/lib2/Foo.pm", "Got the original module, not the 2nd one, and not an error."); chdir "$d/.."; # So File::Temp can remove $d exit(0); # remove the /private from Mac OS X paths sub clean_darwin { my ($path) = @_; $path =~ s#//#/#g; return $path unless $^O eq 'darwin'; $path =~ s{^/private}{}; return $path; } Slimspace.pm000444023532023421 25112121654173 14317 0ustar00abrummetgsc000000000000UR-0.41/tpackage Vending; use warnings; use strict; use UR; class Vending { is => [ 'UR::Namespace' ], doc => 'Used by the namespace_loaded_from_symlink test', }; 1; URT.pm000444023532023421 36612121654173 13060 0ustar00abrummetgsc000000000000UR-0.41/tuse strict; use warnings; package URT; use UR; class URT { is => ['UR::Namespace'], has_constant => [ allow_sloppy_primitives => { value => 1 }, ], doc => 'A dummy namespace used by the UR test suite.', }; 1; #$Header urbenchmark.pl000444023532023421 512312121654174 14723 0ustar00abrummetgsc000000000000UR-0.41/tuse Time::HiRes; my $n_props = shift(@ARGV) || 5; my $n = shift(@ARGV) || 100_000; print STDERR "using classes with $n_props properties\n"; print STDERR "testing on $n objects\n\n"; my @pnames = map { "p$_" } (1..$n_props-1); package UrObj; use UR; class UrObj { has => [@pnames] }; # simulate a million-item table with "1" in each column sub __load__ { my $data = IO::File->new("perl -e 'my \$id = 1; while(\$id <= $n) { print \$id++,qq|\n| }' |"); my $iterator = sub { my $v = $data->getline; chomp $v; if (not defined $v or $v == $n) { $data->close(); return; } return [$v,$v,$v,$v]; }; return (['id',@pnames], $iterator); } package MooseObj; use Moose; has 'id' => (is => 'ro'); for (@pnames) { has $_ => (is => 'rw'); } push @pnames, 'id'; my @pvalues; $#pvalues = $#pnames; my %p; package main; my @t = ( '$o = bless({ @_ },"PerlObj")', #'$o = UR::BoolExpr->resolve("UrObj",@_)', #'$o = bless({ @{ UR::BoolExpr->resolve("UrObj",@_)->{_params_list} } } , "UrObj")', #'$o = bless({ UR::BoolExpr->resolve("UrObj",@_)->_params_list } , "UrObj")', #'do { my $b = UR::BoolExpr->resolve("UrObj",@_); my @p = $b->_params_list; my @pp = $b->template->extend_params_list_for_values(@p); bless({@p, @pp}, "UrObj"); };', #'$o = UR::BoolExpr->resolve_normalized("UrObj",@_)', #'$o = bless({ UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list } , "UrObj")', #'$o = bless({ UR::BoolExpr->resolve_normalized("UrObj",@_)->params_list}, "UrObj")', '@o = UrObj->get()', '$o = UrObj->create(@_)', '$o = MooseObj->new(@_)', #'do { @x = UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list; $o = bless { @x, db_committed => { @x } } , "UrObj"; }; ', #'do { @x = UR::BoolExpr->resolve_normalized("UrObj",@_)->_params_list; $o = bless { @x } , "UrObj"; }; ', ); my @a; $#a = $n; my @x; my $prev_d; for my $t (@t) { my $t1 = Time::HiRes::time(); my $o; my @o; my $s = 'sub { push @a, ' . $t . "}"; print $s,"\n"; my $f = eval $s; die "$@" if $@; if (substr($t,0,1) ne '@') { #print "each...\n"; for (1..$n) { for my $p (@pvalues) { $p = $_ }; @p{@pnames} = @pvalues; $f->(%p); }; } else { #print "bulk...\n"; $f->(); } my $d = Time::HiRes::time()-$t1; my $diff = ($prev_d ? $d/$prev_d : 0); $prev_d = $d; print "$d seconds for $n of: $t\n ...$diff x slower than the prior\n\n"; } package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; CmdTest.pm000555023532023421 36512121654174 13754 0ustar00abrummetgsc000000000000UR-0.41/t#!/usr/bin/env perl package CmdTest; use Command::Tree; class CmdTest { is => 'Command::Tree', doc => 'test suite test command tree' }; if ($0 eq __FILE__) { require Command::Shell; exit Command::Shell->run(__PACKAGE__,@ARGV); } 1; CdExample.pm000444023532023421 12012121654174 14235 0ustar00abrummetgsc000000000000UR-0.41/tpackage CdExample; use UR; class CdExample { is => 'UR::Namespace' }; 1; ur-cachetest.pl000444023532023421 367212121654175 15021 0ustar00abrummetgsc000000000000UR-0.41/tuse Time::HiRes; my $n_props = shift(@ARGV) || 5; my $lw = shift(@ARGV); my $hw = shift(@ARGV); $ENV{UR_CONTEXT_CACHE_SIZE_LOWWATER} = $lw; $ENV{UR_CONTEXT_CACHE_SIZE_HIGHWATER} = $hw; $ENV{UR_DEBUG_OBJECT_PRUNING} = 1; $ENV{UR_DEBUG_OBJECT_RELEASE} = 1; print STDERR "using classes with $n_props properties\n"; print STDERR "low/high water is $lw/$hw\n"; my @pnames = map { "p$_" } (1..$n_props-1); ## require UR; class UrObj { has => [@pnames] }; sub UrObj::__load__ { # an infinite data set (will hang if you don't iterate) my $data = IO::File->new("perl -e 'my \$id = 1; while(1) { print \$id++,qq|\n| }' |"); my $iterator = sub { my $v = $data->getline; chomp $v; if (not defined $v or $v == $n) { $data->close(); return; } return [$v,$v,$v,$v]; }; return (['id',@pnames], $iterator); } ## package main; my $i = UrObj->create_iterator(); my $n = 0; while ($o = $i->next) { $n++; if ($n % 10_000 == 0) { my @o = UrObj->is_loaded(); my $loaded = scalar(@o); @o = (); print STDERR UR::Context->now, ":\t$n objects, with $loaded loaded\n"; } if ($n == 2_000_010) { last; } } __END__ my @a; $#a = $n; my @x; my $prev_d; for my $t (@t) { my $t1 = Time::HiRes::time(); my $o; my @o; my $s = 'sub { push @a, ' . $t . "}"; print $s,"\n"; my $f = eval $s; die "$@" if $@; if (substr($t,0,1) ne '@') { #print "each...\n"; for (1..$n) { for my $p (@pvalues) { $p = $_ }; @p{@pnames} = @pvalues; $f->(%p); }; } else { #print "bulk...\n"; $f->(); } my $d = Time::HiRes::time()-$t1; my $diff = ($prev_d ? $d/$prev_d : 0); $prev_d = $d; print "$d seconds for $n of: $t\n ...$diff x slower than the prior\n\n"; } package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; Vending.pm000444023532023421 27412121654175 14000 0ustar00abrummetgsc000000000000UR-0.41/tpackage Vending; use warnings; use strict; use UR; class Vending { is => [ 'UR::Namespace' ], has_constant => [ allow_sloppy_primitives => { value => 1 }, ] }; 1; alternate_namespace_layout000755023532023421 012121654174 17317 5ustar00abrummetgsc000000000000UR-0.41/tt000755023532023421 012121654174 17562 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout02_update_classes.t000444023532023421 1165312121654172 23430 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/tuse strict; use warnings; use Test::More; use File::Basename; use File::Temp; use Cwd; use lib Cwd::abs_path(File::Basename::dirname(__FILE__)."/../../../lib"); # for UR our $initial_dir; our $tempdir; BEGIN { eval "use Archive::Tar"; if (1) { plan skip_all => 'this always fails during cpanm install for an unknown reason', } elsif ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'skip running during install', exit; } elsif ($@ =~ qr(Can't locate Archive/Tar.pm in \@INC)) { plan skip_all => 'Archive::Tar does not exist on the system'; exit; } else { plan tests => 36; } $initial_dir = Cwd::cwd; my $tarfile = Cwd::abs_path(File::Basename::dirname(__FILE__).'/02_update_classes.tar.gz'); $tempdir = File::Temp::tempdir(CLEANUP => 1); chdir($tempdir); my $tar = Archive::Tar->new($tarfile); ok($tar->extract, 'Extract initial filesystem'); } END { chdir $initial_dir; # so File::Temp can clean up the tempdir } use lib $tempdir.'/namespace'; use lib $tempdir.'/classes'; use lib $tempdir.'/data_source'; use lib $tempdir.'/more_classes'; use URTAlternate; UR::DBI->no_commit(0); # UR's test harness defaults to no_commit = 1 my $cmd = UR::Namespace::Command::Update::ClassesFromDb->create(namespace_name => 'URTAlternate'); ok($cmd, 'Create update classes command'); $cmd->queue_status_messages(1); $cmd->queue_warning_messages(1); $cmd->queue_error_messages(1); $cmd->dump_status_messages(0); $cmd->dump_warning_messages(0); $cmd->dump_error_messages(0); ok($cmd->execute, 'execute update classes with no changes'); my $messages = join("\n",$cmd->status_messages()); like($messages, qr(Updating namespace: URTAlternate), 'Status message showing namespace'); like($messages, qr(Found data sources: TheDB), 'Found the data source'); like($messages, qr(No data schema changes), 'No schema changes'); like($messages, qr(No class changes), 'No class changes'); my @messages = $cmd->warning_messages(); is(scalar(@messages), 0, 'no warning messages'); @messages = $cmd->error_messages(); is(scalar(@messages), 0, 'no error messages'); my $dbh = URTAlternate::DataSource::TheDB->get_default_handle(); ok($dbh, 'Got handle for database'); ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY REFERENCES person(person_id), office varchar NOT NULL)'), 'Add employee table'); ok($dbh->do('ALTER TABLE car ADD COLUMN owner_id integer REFERENCES person(person_id)'), 'Add owner_id column to car table'); ok($dbh->commit, 'commit table changes'); # SQLite seems to have an issue where "PRAGMA foreign_key_list()" doesn't return # the newly added foreign key info from the ALTER TABLE car. Workaround is to disconnect # and re-connect URTAlternate::DataSource::TheDB->disconnect_default_handle(); $dbh = URTAlternate::DataSource::TheDB->get_default_handle(); my $sth = $dbh->prepare("PRAGMA foreign_key_list(car)"); $sth->execute(); my $data = $sth->fetchall_arrayref(); $cmd = UR::Namespace::Command::Update::ClassesFromDb->create(namespace_name => 'URTAlternate', _override_no_commit_for_filesystem_items => 1); ok($cmd, 'Create update classes command after adding table'); $cmd->dump_status_messages(1); $cmd->dump_warning_messages(1); $cmd->dump_error_messages(1); $cmd->dump_status_messages(0); $cmd->dump_warning_messages(0); $cmd->dump_error_messages(0); ok($cmd->execute(), 'execute update classes after changes'); ok(-f "${tempdir}/namespace/URTAlternate.pm", 'Namespace module exists'); ok(-f "${tempdir}/data_source/URTAlternate/DataSource/TheDB.pm", 'Data source module exists'); ok(-f "${tempdir}/classes/URTAlternate/Person.pm", 'Person module exists'); ok(-f "${tempdir}/more_classes/URTAlternate/Car.pm", 'Car module exists'); ok(-f "${tempdir}/namespace/URTAlternate/Employee.pm", 'Employee module exists'); # new stuff gets created in the namespace dir foreach my $class_props ( [ 'URTAlternate::Person' => ['person_id', 'name'] ], [ 'URTAlternate::Car' => ['car_id', 'make', 'model', 'owner_id', 'person_owner'] ], [ 'URTAlternate::Employee' => ['employee_id', 'office', 'person_employee'] ] ) { my($class_name, $expected_properties) = @$class_props; my $class_meta = $class_name->__meta__; ok($class_meta, "Got class metaobject for $class_name"); my %expected_properties = map { $_ => 1 } @$expected_properties; my %got_properties = map { $_->property_name => 1 } UR::Object::Property->get(class_name => $class_name); foreach my $property ( keys %expected_properties ) { ok(delete $got_properties{$property}, "Found property $property for class $class_name"); } ok(!scalar(keys %got_properties), 'no extra properties'); if (keys %got_properties) { diag('Extra properties that were not expected: ', join(', ', keys %got_properties)); } } 01_namespace.t000444023532023421 341312121654174 22341 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../namespace'; use lib File::Basename::dirname(__FILE__).'/../classes'; use lib File::Basename::dirname(__FILE__).'/../data_source'; use lib File::Basename::dirname(__FILE__).'/../more_classes'; use URTAlternate; is(URTAlternate->class, 'URTAlternate', 'Namespace name'); my $class_meta = URTAlternate->get_member_class('URTAlternate::Person'); ok($class_meta, 'get_member_class'); is($class_meta->class_name, 'URTAlternate::Person', 'get_member_class returned the right class'); # This is basically the list of Perl modules under URT/ # note that the 38* classes do not compile because they use data sources that exist # only during that test, and so are not returned by get_material_classes() my @expected_class_names = sort map { 'URTAlternate::' . $_ } qw( Person Car DataSource::Meta DataSource::TheDB Vocabulary ); my @class_metas = sort URTAlternate->get_material_classes; is(scalar(@class_metas), scalar(@expected_class_names), 'get_material_classes returned expected number of items'); foreach (@class_metas) { isa_ok($_, 'UR::Object::Type'); } my @class_names = sort map { $_->class_name } @class_metas; is_deeply(\@class_names, \@expected_class_names, 'get_material_classes'); my @data_sources = sort URTAlternate->get_data_sources; foreach ( @data_sources) { isa_ok($_, 'UR::DataSource'); } my @expected_ds_names = map { 'URTAlternate::' . $_ } qw( DataSource::Meta DataSource::TheDB ); my @data_source_names = sort map { $_->class } @data_sources; is_deeply(\@data_source_names, \@expected_ds_names, 'get_data_sources'); data_source000755023532023421 012121654172 21606 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layoutURTAlternate000755023532023421 012121654172 24120 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_sourceDataSource000755023532023421 012121654175 26155 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_source/URTAlternateTheDB.pm000444023532023421 61312121654172 27553 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_source/URTAlternate/DataSourcepackage URTAlternate::DataSource::TheDB; use strict; use warnings; use File::Temp; use URTAlternate; class URTAlternate::DataSource::TheDB { is => ['UR::DataSource::SQLite'], }; sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . '_ur_testsuite_db' . $self->_extension_for_db; return $PATH; } END { our $PATH; unlink $PATH if $PATH; } 1; Meta.sqlite3-dump000444023532023421 645012121654174 31455 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_source/URTAlternate/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('URTAlternate::DataSource::TheDB','main','car','car_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('URTAlternate::DataSource::TheDB','main','person','person_id',1); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('URTAlternate::DataSource::TheDB','main','car','TABLE','entity',NULL,'2011-04-20 17:07:18',NULL); INSERT INTO "dd_table" VALUES('URTAlternate::DataSource::TheDB','main','person','TABLE','entity',NULL,'2011-04-20 17:07:18',NULL); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','model','varchar',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','person','name','varchar',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','car_id','integer',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','person','person_id','integer',NULL,'N','2011-04-20 17:07:18',''); INSERT INTO "dd_table_column" VALUES('URTAlternate::DataSource::TheDB','main','car','make','varchar',NULL,'N','2011-04-20 17:07:18',''); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; TheDB.sqlite3-dump000444023532023421 27412121654175 31474 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_source/URTAlternate/DataSourceCREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, name varchar NOT NULL); CREATE TABLE car (car_id integer NOT NULL PRIMARY KEY, make varchar NOT NULL, model varchar NOT NULL); Meta.pm000444023532023421 114012121654175 27532 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/data_source/URTAlternate/DataSourcepackage URTAlternate::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use UR; UR::Object::Type->define( class_name => 'URTAlternate::DataSource::Meta', is => ['UR::DataSource::Meta'], ); use File::Temp; # Override server() so we can make the metaDB file in # a temp dir sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . "_ur_testsuite_metadb" . $self->_extension_for_db; return $PATH; } END { our $PATH; unlink $PATH if ($PATH); } 1; namespace000755023532023421 012121654172 21251 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layoutURTAlternate.pm000444023532023421 27012121654172 24235 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/namespaceuse strict; use warnings; package URTAlternate; use UR; class URTAlternate { is => ['UR::Namespace'], doc => 'A dummy namespace used by the UR test suite.', }; 1; #$Header classes000755023532023421 012121654173 20753 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layoutURTAlternate000755023532023421 012121654174 23266 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/classesPerson.pm000444023532023421 60412121654173 25206 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/classes/URTAlternatepackage URTAlternate::Person; use URTAlternate; use strict; use warnings; class URTAlternate::Person { table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, ], schema_name => 'TheDB', data_source => 'URTAlternate::DataSource::TheDB', }; sub uc_name { return uc(shift->name); } 1; Vocabulary.pm000444023532023421 51712121654174 26053 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/classes/URTAlternatepackage URTAlternate::Vocabulary; use strict; use warnings; use UR::Object::Type; use URTAlternate; class URTAlternate::Vocabulary { is => ['UR::Vocabulary'], doc => 'A set of words for a given namespace.', }; my @words_with_special_case = (qw//); sub _words_with_special_case { return @words_with_special_case; } 1; more_classes000755023532023421 012121654174 21776 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layoutURTAlternate000755023532023421 012121654174 24310 5ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/more_classesCar.pm000444023532023421 56212121654174 25473 0ustar00abrummetgsc000000000000UR-0.41/t/alternate_namespace_layout/more_classes/URTAlternatepackage URTAlternate::Car; use URTAlternate; use strict; use warnings; class URTAlternate::Car { table_name => 'car', id_by => [ car_id => { is => 'integer' }, ], has => [ make => { is => 'varchar' }, model => { is => 'varchar' }, ], schema_name => 'TheDB', data_source => 'URTAlternate::DataSource::TheDB', }; 1; URT000755023532023421 012121654175 12402 5ustar00abrummetgsc000000000000UR-0.41/t34Baseclass.pm000444023532023421 42412121654172 15121 0ustar00abrummetgsc000000000000UR-0.41/t/URT package URT::34Baseclass; use strict; use warnings; use URT; class URT::34Baseclass { is_transactional => 0, has => [ parent => { is => 'URT::34Subclass', id_by => 'parent_id' }, thingy => { is => 'URT::Thingy', id_by => 'thingy_id' } ] }; 1; 43Related.pm000444023532023421 67512121654173 14612 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::43Related; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::43Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::43Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { via => 'primary_objects', to => 'primary_value', is_many => 1}, ], ); 1; 34Subclass.pm000444023532023421 64112121654173 15002 0ustar00abrummetgsc000000000000UR-0.41/t/URT package URT::34Subclass; use strict; use warnings; ## dont "use URT::34Baseclass"; use URT; class URT::34Subclass { isa => 'URT::34Baseclass', is_transactional => 0, has => [ some_other_stuff => { is => 'SCALAR' }, abcdefg => { } ] }; sub create { my $class = shift; my $self = $class->SUPER::create( thingy => URT::Thingy->create ); return $self; } 1; Thingy.pm000444023532023421 174712121654174 14347 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::Thingy; use warnings; use strict; use UR::Object::Type; use URT; class URT::Thingy { id_by => [ pcr_id => { is => 'NUMBER', len => 15 }, ], has => [ enz_id => { is => 'NUMBER', len => 10, doc => "Link to polymerase used in PCR." }, pcr_name => { is => 'VARCHAR2', len => 64, doc => "GSC name of the pcr_product. Named based on documented naming conventions." }, pri_id_1 => { is => 'NUMBER', len => 10, doc => "Link to one primer used in PCR." }, pri_id_2 => { is => 'NUMBER', len => 10, doc => "Link to one primer used in PCR." }, ], unique_constraints => [ { properties => [qw/pcr_name/], sql => 'PCR_NAME_U' }, ], doc => 'Stores information for each instance of a polymerase chain react', }; 1; 43Primary.pm000444023532023421 67412121654174 14655 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::43Primary; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::43Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, rel_id => { is => 'Integer'}, related_object => { is => 'URT::43Related', id_by => 'rel_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], ); 1; 38Primary.pm000444023532023421 102212121654174 14665 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::38Primary; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::38Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, rel_id => { is => 'Integer'}, related_object => { is => 'URT::38Related', id_by => 'rel_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], data_source => 'URT::DataSource::SomeSQLite1', table_name => 'primary_table', ); 1; RAMThingy.pm000444023532023421 16312121654174 14656 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::RAMThingy; use warnings; use strict; use UR::Object::Type; use URT; class URT::RAMThingy { }; 1; ObjWithHash.pm000444023532023421 27612121654175 15234 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::ObjWithHash; use warnings; use strict; use URT; class URT::ObjWithHash { has => [ myhash1 => { is => 'HASH' }, mylist => { is => 'ARRAY' }, ], }; 1; Vocabulary.pm000444023532023421 46512121654175 15171 0ustar00abrummetgsc000000000000UR-0.41/t/URT package URT::Vocabulary; use strict; use warnings; use UR::Object::Type; use URT; class URT::Vocabulary { is => ['UR::Vocabulary'], doc => 'A set of words for a given namespace.', }; my @words_with_special_case = (qw//); sub _words_with_special_case { return @words_with_special_case; } 1; 38Related.pm000444023532023421 101512121654175 14625 0ustar00abrummetgsc000000000000UR-0.41/t/URTpackage URT::38Related; use URT; use strict; use warnings; UR::Object::Type->define( class_name => 'URT::38Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::38Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { via => 'primary_objects', to => 'primary_value', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite2', table_name => 'related', ); 1; t000755023532023421 012121654175 12645 5ustar00abrummetgsc000000000000UR-0.41/t/URT87d_query_by_is_many_indirect_is_efficient.t000444023532023421 1403312121654172 23635 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table MAINTABLE ( main_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table RELATED1 (related1_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES maintable(main_id), value varchar)'), 'created related1 table'); ok($dbh->do('create table RELATED2 (related2_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related1(related1_id), value varchar)'), 'created related2 table'); ok($dbh->do('create table RELATED3 (related3_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related2(related2_id), value varchar)'), 'created related3 table'); ok($dbh->do('create table RELATED4 (related4_id int NOT NULL PRIMARY KEY, related_id integer REFERENCES related3(related3_id), value varchar)'), 'created related4 table'); $dbh->do("insert into maintable values (1,'Bob')"); $dbh->do("insert into related1 values (1,1,'related1')"); $dbh->do("insert into related2 values (1,1,'related2')"); $dbh->do("insert into related3 values (1,1,'related3')"); $dbh->do("insert into related4 values (1,1,'related4')"); $dbh->do("insert into maintable values (2,'Joe')"); $dbh->do("insert into related1 values (2,2,'related1alt')"); $dbh->do("insert into related2 values (2,2,'related2alt')"); $dbh->do("insert into related3 values (2,2,'related3alt')"); $dbh->do("insert into related4 values (2,2,'related4alt')"); ok(UR::Object::Type->define( class_name => 'URT::Main', table_name => 'maintable', id_by => [ main_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, ], has_many => [ related_1s => { is => 'URT::Related1', reverse_as => 'related' }, related_values => { via => 'related_1s', to => 'value' }, related_2s => { is => 'URT::Related2', via => 'related_1s', to => 'related2s' }, related_2_values => { via => 'related_2s', to => 'value' }, related_3s => { is => 'URT::Related3', via => 'related_2s', to => 'related3s' }, related_3_values => { via => 'related_3s', to => 'value' }, related_4s => { is => 'URT::Related4', via => 'related_3s', to => 'related4s' }, related_4_values => { via => 'related_4s', to => 'value' }, related_4_values_alt => { via => 'related_1s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for main'); ok(UR::Object::Type->define( class_name => 'URT::Related1', table_name => 'related1', id_by => [ related1_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Main', id_by => 'related_id' }, value => { is => 'string' }, related2s => { is => 'URT::Related2', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related2s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 1"); ok(UR::Object::Type->define( class_name => 'URT::Related2', table_name => 'related2', id_by => [ related2_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related1', id_by => 'related_id' }, value => { is => 'string' }, related3s => { is => 'URT::Related3', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related3s', to => 'related_4_values_alt' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 2"); ok(UR::Object::Type->define( class_name => 'URT::Related3', table_name => 'related3', id_by => [ related3_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related2', id_by => 'related_id' }, value => { is => 'string' }, related4s => { is => 'URT::Related4', reverse_as => 'related', is_many => 1}, related_4_values_alt => { via => 'related4s', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 3"); ok(UR::Object::Type->define( class_name => 'URT::Related4', table_name => 'related4', id_by => [ related4_id => { is => 'NUMBER' }, ], has => [ related => { is => 'URT::Related3', id_by => 'related_id' }, value => { is => 'string' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for related 4"); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $thing; $query_count = 0; #$DB::single=1; $thing = URT::Main->get(related_4_values => 'related4'); ok($thing, 'Got one object for a 5-table join'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Related1->get(related_id => 1); ok($thing, 'Got 1 related URT::Related1 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related2->get(related_id => 1); ok($thing, 'Got 1 related URT::Related2 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related3->get(related_id => 1); ok($thing, 'Got 1 related URT::Related3 thing by related_id'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing = URT::Related4->get(related_id => 1, value => 'related4'); ok($thing, 'Got 1 related URT::Related4 thing by related_id'); is($query_count, 0, 'Made no queries'); 85b_avoid_loading_using_hints.t000444023532023421 650012121654172 21054 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # This tests a bugfix where specifying a hints to a property that # includes a where-clause would omit the where params in the template/rule # that gets recorded in the query cache. As a result, a later query could # incorrectly think it had already been loaded and miss data. use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1, }, primary_car => { is => 'URT::Car', reverse_as => 'owner', where => ['is_primary true' => 1], is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my $person = URT::Person->get(name => 'Bob', -hints => ['primary_car_color']); ok($person, 'Got a person named Bob'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my $color = $person->primary_car_color(); is($color, 'yellow', "Bob's primary car color is yellow"); is($query_count, 0, 'Made no queries'); $query_count = 0; my @cars = URT::Car->get(owner_id => $person->id); is(scalar(@cars), 2, 'Bob has 2 cars'); is($query_count, 1, 'Made 1 query'); 60_sql_query_hint.t000444023532023421 655112121654172 16546 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Make sure query_hint and join_hint in class metadata appear in the generated SQL use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', select_hint => '/* person hint */', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1, }, primary_car => { is => 'URT::Car', reverse_as => 'owner', where => ['is_primary true' => 1], is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', query_hint => '/* car hint */', # query_hint is an alias for select_hint join_hint => '/* car join hint */', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { my($ds, $method, $query) = @_; $query_text = $query; $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(1); is(scalar(@p), 1, 'Got one person'); like($query_text, qr(/\* person hint \*/), 'Saw the person hint'); @p = URT::Person->get(id => 2, -hint => ['cars']); is(scalar(@p), 1, 'Got a different person'); like($query_text, qr(/\* person hint car join hint \*/), 'Saw both hints'); my @c = URT::Car->get(id => 5); is(scalar(@c), 1, 'Got one car'); like($query_text, qr(/\* car hint \*/), 'Saw the car hint'); 47b_indirect_is_many_accessor_mutable_with_id_class_by.t000444023532023421 2401012121654172 26156 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 84; UR::Object::Type->define( class_name => 'URT::Value1', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Value2', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Value3', has => [ p1 => { is => 'Text', is_optional => 1 }, ] ); UR::Object::Type->define( class_name => 'URT::Param', id_by => [ thing_id => { is => 'Number' }, name => { is => 'String' }, value_class_name => { is => 'Text' }, value_id => { is => 'Text' }, ], has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'UR::Object', id_class_by => 'value_class_name', id_by => 'value_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Number' }, ], has => [ params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1 }, param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1 }, # Actually, either of these property definitions will work interesting_param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1, where => [ name => 'interesting'] }, #interesting_params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1, # where => [name => 'interesting']}, #interesting_param_values => { via => 'interesting_params', to => 'value', is_many => 1, is_mutable => 1 }, #< Test adding primitives, giving the class name friends => { via => 'params', to => 'value_id', is_many => 1, is_mutable => 1, where => [qw/ name friends value_class_name UR::Value /], }, ], ); my $v1 = URT::Value1->create(1); ok($v1, "made a test value 1"); my $v2 = URT::Value2->create(2); ok($v2, "made a test value 2"); my $v3 = URT::Value3->create(3); ok($v3, "made a test value 3"); ok("URT::Param"->can("value_id"), "created a property for value_id implicitly"); ok("URT::Param"->can("value_class_name"), "created a property for value_class_name implicitly"); #$DB::single = 1; #my $o1 = URT::Thing->create(thing_id => 2, param_values => [$v2,$v3]); my $o1 = URT::Thing->create(thing_id => 1); ok($o1, "created a test object which has-many of a test property"); #<># # test by direct construction of the bridge my $p = URT::Param->create(thing_id => 1, name => 'uninteresting', value => $v1); ok($p, "made an object with a value as a paramter"); is($p->value_class_name, ref($v1), "class name is set on the new object as expected"); is($p->value_id, $v1->id, "id is set on the new object as expected"); #$DB::single = 1; is($p->value,$v1,"got the value back"); my @p = $o1->params(); is(scalar(@p),1,"got a param"); is($p[0],$p, "got the expected param back"); my @pv = $o1->param_values(); is(scalar(@pv),1,"got a param value"); is($pv[0],$v1,"got expected value"); #<># note('test "add_param"'); my $p2 = $o1->add_param(name => 'interesting', value => $v2); ok($p2, "added param 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_param"'); #$DB::single = 1; ok($o1->remove_param($p2), "removed param 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_param_value"'); #$DB::single = 1; $p2 = $o1->add_param_value(name => 'interesting', value => $v2); ok($p2, "added another param"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_param_value"'); #$DB::single = 1; ok($o1->remove_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_interesting_param_value" with a key-value pair'); #$DB::single = 1; $p2 = $o1->add_interesting_param_value(value => $v2); ok($p2, "added an intereting param"); is($p2->name,'interesting', "the param name was set automatically during addition"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_interesting_param_value"'); #$DB::single = 1; ok($o1->remove_interesting_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># note('test "add_interesting_param_value" without a key-value pair'); #$DB::single = 1; $p2 = $o1->add_interesting_param_value($v2); ok($p2, "added an intereting param"); is($p2->name,'interesting', "the param name was set automatically during addition"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),2,"got two params"); is($p[0],$p, "got the expected param 1 back"); is($p[1],$p2, "got the expected param 2 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),2,"got two param values"); is($pv[0],$v1,"got expected value 1"); is($pv[1],$v2,"got expected value 2"); #<># note('test "remove_interesting_param_value" again'); #$DB::single = 1; ok($o1->remove_interesting_param_value($v2), "removed param value 2"); @p = sort { $a->value_id <=> $b->value_id } $o1->params(); is(scalar(@p),1,"got one param after removing param 2"); is($p[0],$p, "got the expected param 1 back"); @pv = sort { $a->id <=> $b->id } $o1->param_values(); is(scalar(@pv),1, "got one param value after removeing param 2"); is($pv[0],$v1,"got expected value 1"); #<># #note("test setting an indirect value as a group"); #$o1->interesting_param_values(undef); #my @v = $o1->interesting_param_values; #ok(!@v, "no values associated after setting value to undef through has-many mutable accessor") # or diag(Data::Dumper::Dumper(\@v)); #@v = $o1->interesting_param_values([$v1,$v2,$v3]); #is("@v", "$v1 $v2 $v3", "correctly re-set the value list"); #<># #$DB::single = 1; my $thing2 = URT::Thing->create(thing_id => 2, interesting_param_values => [$v1,$v2,$v3]); ok($thing2, 'Created another Thing'); my @params = $thing2->params();; is(scalar(@params), 3, 'And it has 3 attached params'); isa_ok($params[0], 'URT::Param'); isa_ok($params[1], 'URT::Param'); isa_ok($params[2], 'URT::Param'); @params = sort { $a->value cmp $b->value } @params; is($params[0]->name, 'interesting', "param 1's name is interesting"); is($params[1]->name, 'interesting', "param 2's name is interesting"); is($params[2]->name, 'interesting', "param 3's name is interesting"); is($params[0]->value, $v1, "param 1's value is correct"); is($params[1]->value, $v2, "param 2's value is correct"); is($params[2]->value, $v3, "param 3's value is correct"); $v1->p1(1000); my @values = $thing2->param_values(p1 => 1000); is(scalar(@values), 1, "got one object back when filtering in an indirect accessor which is two steps away"); is($values[0], $v1, "got the correct object back when filtering in an indirect accessor which his two steps away"); @values = $thing2->param_values(); is(scalar(@values), 3, "got everything back when not filtering with an indirect accessor which is two steps away"); # Try to get the object again w/ id my $o2 = URT::Thing->get(2); ok($o2, 'Got thingy w/ id 2'); my @v = $o2->interesting_param_values; @v = sort { $a->id cmp $b->id } @v; my @expected = sort { $a->id cmp $b->id } ( $v1, $v2, $v3 ); is_deeply(\@v,\@expected, 'Ineresting values match those from orginal object'); #is_deeply([ $o1->interesting_param_values ], [ $thing2->interesting_param_values ], 'Ineresting values match those from orginal object'); #<># note('primitives with UR::Value in where clause'); $o1->add_friend('Watson'); is_deeply([$o1->friends], [qw/ Watson /], 'Added a friend: Watson'); $o1->add_friend('Crick'); is_deeply([sort $o1->friends], [qw/ Crick Watson /], 'Added a friend: Crick'); $o1->remove_friend('Watson'); is_deeply([$o1->friends], [qw/ Crick /], 'Removed a friend: Watson'); $o1->friends(undef); ok(!$o1->friends, 'Set friends to undef'); # Try to get the object again w/ id and ineresting values # FIXME does not work #my $o3 = URT::Thing->get( # thing_id => 2, # interesting_param_values => ['abc','def'], #); #ok($o3, 'Got thingy w/ id 2 and interesting_param_values => [qw/abc def/]'); #is_deeply([ $o->interesting_param_values ], [ $o3->interesting_param_values ], 'Ineresting values match those from original object'); 76_is_many_default_values.t000444023532023421 245712121654172 20232 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Data::Dumper; use Test::More tests => 6; class Spy { has => [ name => { is => 'Text', default_value => 'James Bond', }, aliases => { is => 'Text', is_many => 1, default_value => ['007', 'Bond', 'James Bond'], }, ], }; { # Test Default Values my $spy = Spy->create(); isa_ok($spy, 'Spy'); ok($spy->name eq 'James Bond', "Spy's default name is correct"); my $default_aliases = '007|Bond|James Bond'; my $aliases = join('|', sort($spy->aliases)); #print "Aliases: $aliases\nExpected Aliases: $default_aliases\n"; ok($aliases eq $default_aliases, "Spy's default aliases are correct"); } { # Test Specified Values my $name = 'Margaretha Geertruida (Grietje) Zelle'; my $alias = 'Mata Hari'; my $spy = Spy->create(name => $name, aliases => [$alias]); isa_ok($spy, 'Spy'); ok($spy->name eq $name, "Spy's name is correct"); my $aliases = join('|', sort($spy->aliases)); #print "Aliases: $aliases\nExpected Aliases: $alias\n"; ok($aliases eq $alias, "Spy's aliases are correct"); } { # TODO: Test complex default values involving database bridges? ; } mro.t000444023532023421 2510312121654172 14002 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use UR; use MRO::Compat; setup_test_env(); test_namespace_valid_values_mro(); test_namespace_gets_default_mro(); test_gryphon_object_methods_follow_dfs_mro(); test_gryphon_inheritance_follows_dfs_mro(); test_class_property_follows_dfs_mro(); test_package_sub_follows_dfs_mro(); if ($^V lt 5.9.5) { my $namespace = C3Animal->get(); is($namespace->method_resolution_order, 'dfs', 'MRO reverted to DFS on a C3 namespace if Perl < 5.9.5'); note('Skipping C3 tests because Perl < 5.9.5.'); } else { test_gryphon_object_methods_follow_c3_mro(); test_gryphon_inheritance_follows_c3_mro(); test_class_property_follows_c3_mro(); test_package_sub_follows_c3_mro(); } done_testing(); sub test_namespace_valid_values_mro { my $namespace = Animal->get(); my $property = $namespace->__meta__->property('method_resolution_order'); if ($^V lt 5.9.5) { is_deeply($property->valid_values, ['dfs'], 'valid MRO for Perl < 5.9.5 is only DFS'); } else { is_deeply($property->valid_values, ['dfs', 'c3'], 'valid MRO for Perl >= 5.9.5 is DFS and C3'); } } sub test_namespace_gets_default_mro { my $animal_namespace = Animal->get(); isa_ok($animal_namespace, 'UR::Namespace', 'got Animal namespace'); # This is meant to check that the namespace has the default value of method_resolution_order # populated on get unlike non-singleton objects which are only populated on create. ok($animal_namespace->can('method_resolution_order'), 'namespace can method_resolution_order') || return; ok($animal_namespace->method_resolution_order, 'namespace has a method_resolution_order'); } ####################### # DFS Based Namespace # ####################### sub test_gryphon_object_methods_follow_dfs_mro { my $animal = DfsAnimal::Animal->create(); my $lion = DfsAnimal::Lion->create(); my $eagle = DfsAnimal::Eagle->create(); my $gryphon = DfsAnimal::Gryphon->create(); is($lion->foo, $animal->foo, "Lion's foo is the same as Animal's"); isnt($eagle->foo, $animal->foo, "Eagle's foo is not the same as Animal's"); is($gryphon->foo, $animal->foo, "Gryphon's foo is the same as Animal's"); } sub test_gryphon_inheritance_follows_dfs_mro { my $gryphon = DfsAnimal::Gryphon->create(); isa_ok($gryphon, 'DfsAnimal::Gryphon', '$gryphon isa DfsAnimal::Gryphon'); isa_ok($gryphon, 'DfsAnimal::Lion', '$gryphon isa DfsAnimal::Lion'); isa_ok($gryphon, 'DfsAnimal::Eagle', '$gryphon isa DfsAnimal::Eagle'); is(mro::get_mro('DfsAnimal::Gryphon'), 'dfs', "Gryphon's MRO is DFS"); my $i = 0; my $mro_linear_isa = mro::get_linear_isa('DfsAnimal::Gryphon'); my %inheritance = map { $_ => $i++ } @$mro_linear_isa; ok($inheritance{'DfsAnimal::Lion'} < $inheritance{'DfsAnimal::Eagle'}, 'Lion is higher precendence than Eagle'); ok($inheritance{'DfsAnimal::Eagle'} > $inheritance{'UR::Object'}, 'Eagle is lower precendence than UR::Object'); } sub test_class_property_follows_dfs_mro { # This is theoretically the same check as comparing $gryphon->foo to $eagle->foo # However, it appears that property resolution is different than method resolution # since property resolution is done by hand and is probably a breadth first search. my $meta = UR::Object::Type->get(class_name => 'DfsAnimal::Gryphon'); my $foo_property_meta = $meta->property_meta_for_name('foo'); is($foo_property_meta->class_name, 'DfsAnimal::Eagle', "Gryphon is using Eagle's foo"); my $foo_property = $meta->property('foo'); is($foo_property->class_name, 'DfsAnimal::Eagle', "Gryphon is using Eagle's foo"); } sub test_package_sub_follows_dfs_mro { is(DfsAnimal::Animal->species(), 'Animal', "Make sure we installed species sub in Animal"); is(DfsAnimal::Eagle->species(), 'Eagle', "Make sure we installed species sub in Eagle"); is(DfsAnimal::Gryphon->species(), 'Animal', "Gryphon called Animal's species sub"); } ###################### # C3 Based Namespace # ###################### sub test_gryphon_object_methods_follow_c3_mro { my $animal = C3Animal::Animal->create(); my $lion = C3Animal::Lion->create(); my $eagle = C3Animal::Eagle->create(); my $gryphon = C3Animal::Gryphon->create(); is($lion->foo, $animal->foo, "Lion's foo is the same as Animal's"); isnt($eagle->foo, $animal->foo, "Eagle's foo is not the same as Animal's"); is($gryphon->foo, $eagle->foo, "Gryphon's foo is the same as Eagle's"); } sub test_gryphon_inheritance_follows_c3_mro { my $gryphon = C3Animal::Gryphon->create(); isa_ok($gryphon, 'C3Animal::Gryphon', '$gryphon isa C3Animal::Gryphon'); isa_ok($gryphon, 'C3Animal::Lion', '$gryphon isa C3Animal::Lion'); isa_ok($gryphon, 'C3Animal::Eagle', '$gryphon isa C3Animal::Eagle'); is(mro::get_mro('C3Animal::Gryphon'), 'c3', "Gryphon's MRO is C3"); my $i = 0; my $mro_linear_isa = mro::get_linear_isa('C3Animal::Gryphon'); my %inheritance = map { $_ => $i++ } @$mro_linear_isa; ok($inheritance{'C3Animal::Lion'} < $inheritance{'C3Animal::Eagle'}, 'Lion is higher precendence than Eagle'); ok($inheritance{'C3Animal::Eagle'} < $inheritance{'UR::Object'}, 'Eagle is higher precendence than UR::Object'); } sub test_class_property_follows_c3_mro { # This is theoretically the same check as comparing $gryphon->foo to $eagle->foo # However, it appears that property resolution is different than method resolution # since property resolution is done by hand and is probably a breadth first search. my $meta = UR::Object::Type->get(class_name => 'C3Animal::Gryphon'); my $foo_property_meta = $meta->property_meta_for_name('foo'); is($foo_property_meta->class_name, 'C3Animal::Eagle', "Gryphon is using Eagle's foo"); my $foo_property = $meta->property('foo'); is($foo_property->class_name, 'C3Animal::Eagle', "Gryphon is using Eagle's foo"); } sub test_package_sub_follows_c3_mro { is(C3Animal::Animal->species(), 'Animal', "Make sure we installed species sub in Animal"); is(C3Animal::Eagle->species(), 'Eagle', "Make sure we installed species sub in Eagle"); is(C3Animal::Gryphon->species(), 'Eagle', "Gryphon called Eagle's species sub"); } sub setup_test_env { no warnings 'once'; my $animal_namespace_type = UR::Object::Type->define( class_name => 'Animal', is => 'UR::Namespace', ); isa_ok($animal_namespace_type, 'UR::Object::Type', 'defined Animal namespace'); ####################### # DFS Based Namespace # ####################### my $dfs_animal_namespace_type = UR::Object::Type->define( class_name => 'DfsAnimal', is => 'UR::Namespace', has => [ method_resolution_order => { is => 'Text', default_value => 'dfs', }, ], ); isa_ok($dfs_animal_namespace_type, 'UR::Object::Type', 'defined DfsAnimal namespace'); my $dfs_animal_namespace = DfsAnimal->get(); isa_ok($dfs_animal_namespace, 'UR::Namespace', 'got DfsAnimal namespace'); is($dfs_animal_namespace->method_resolution_order, 'dfs', "DfsAnimal's MRO is DFS"); my $dfs_animal_type = UR::Object::Type->define( class_name => 'DfsAnimal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Animal'; ), }, ], ); isa_ok($dfs_animal_type, 'UR::Object::Type', 'defined Animal'); is($dfs_animal_type->namespace, 'DfsAnimal', 'DfsAnimal::Animal is in Animal namespace'); *DfsAnimal::Animal::species = sub { 'Animal' }; my $dfs_lion_type = UR::Object::Type->define( class_name => 'DfsAnimal::Lion', is => 'DfsAnimal::Animal', ); isa_ok($dfs_lion_type, 'UR::Object::Type', 'defined DfsAnimal::Lion'); my $dfs_eagle_type = UR::Object::Type->define( class_name => 'DfsAnimal::Eagle', is => 'DfsAnimal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Eagle'; ), }, ], ); isa_ok($dfs_eagle_type, 'UR::Object::Type', 'defined DfsAnimal::Eagle'); no warnings 'redefine'; *DfsAnimal::Eagle::species = sub { 'Eagle' }; use warnings 'redefine'; my $dfs_gryphon_type = UR::Object::Type->define( class_name => 'DfsAnimal::Gryphon', is => ['DfsAnimal::Lion', 'DfsAnimal::Eagle'], ); isa_ok($dfs_gryphon_type, 'UR::Object::Type', 'defined DfsAnimal::Gryphon'); ###################### # C3 Based Namespace # ###################### my $c3_animal_namespace_type = UR::Object::Type->define( class_name => 'C3Animal', is => 'UR::Namespace', has => [ method_resolution_order => { is => 'Text', default_value => 'c3', }, ], ); isa_ok($c3_animal_namespace_type, 'UR::Object::Type', 'defined C3Animal namespace'); my $c3_animal_namespace = C3Animal->get(); isa_ok($c3_animal_namespace, 'UR::Namespace', 'got C3Animal namespace'); is($c3_animal_namespace->method_resolution_order, 'c3', "C3Animal's MRO is C3"); my $c3_animal_type = UR::Object::Type->define( class_name => 'C3Animal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Animal'; ), }, ], ); isa_ok($c3_animal_type, 'UR::Object::Type', 'defined Animal'); is($c3_animal_type->namespace, 'C3Animal', 'C3Animal::Animal is in Animal namespace'); *C3Animal::Animal::species = sub { 'Animal' }; my $c3_lion_type = UR::Object::Type->define( class_name => 'C3Animal::Lion', is => 'C3Animal::Animal', ); isa_ok($c3_lion_type, 'UR::Object::Type', 'defined C3Animal::Lion'); my $c3_eagle_type = UR::Object::Type->define( class_name => 'C3Animal::Eagle', is => 'C3Animal::Animal', has => [ foo => { is_constant => 1, calculate => q( return 'Eagle'; ), }, ], ); isa_ok($c3_eagle_type, 'UR::Object::Type', 'defined C3Animal::Eagle'); no warnings 'redefine'; *C3Animal::Eagle::species = sub { 'Eagle' }; use warnings 'redefine'; my $c3_gryphon_type = UR::Object::Type->define( class_name => 'C3Animal::Gryphon', is => ['C3Animal::Lion', 'C3Animal::Eagle'], ); isa_ok($c3_gryphon_type, 'UR::Object::Type', 'defined C3Animal::Gryphon'); use warnings 'once'; } 70c_command_tree_usage_text.t000444023532023421 317612121654172 20531 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 2; use IO::File; UR::Object::Type->define( class_name => 'URT::ParentCommand', is => 'Command::Tree', ); { no warnings 'once'; $URT::ParentCommand::SUB_COMMAND_MAPPING = { 'command-a' => 'URT::CommandA', 'command-b' => 'URT::CommandB', }; } UR::Object::Type->define( class_name => 'URT::CommandA', is => 'Command::V2', has => [ param_a => { is => 'String', is_optional => 0 }, param_c => { is => 'String', doc => 'Documentation for param c' }, ], doc => 'This is command a', ); UR::Object::Type->define( class_name => 'URT::CommandB', is => 'Command::V2', has => [ param_a => { is => 'String', is_optional => 0 }, param_b => { is => 'String', doc => 'Documentation for param b' }, ], doc => 'This is command b', ); my $buffer = ''; close STDERR; my $stdout = open(STDERR,'>',\$buffer) || die "Can't redirect stdout to a string"; my $rv = URT::ParentCommand->_execute_with_shell_params_and_return_exit_code(); close STDERR; open(STDERR, ">-") || die "Can't dup original stdout: $!"; STDERR->autoflush(1); ok($rv, 'Parent command executes'); $buffer =~ s/\x{1b}.*?m//mg; # Remove ANSI escape sequences for color/underline my $expected = q(Sub-commands for u-r-t parent-command: command-a This is command a command-b This is command b ERROR: Please specify valid params for 'u-r-t parent-command'. ); is($buffer, $expected, 'Output with no params was as expected'); 86b-custom-load-join.t000444023532023421 254712121654172 16755 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use UR; use Test::More tests => 8; note("*** class 1: like-clause ***"); class Acme::Foo { has => [qw/a b c/] }; sub Acme::Foo::__load__ { return [qw/id a b c/], [ [100, "a100", "b100", "c100"], [200, "a200", "b200", "c200"], [300, "a300", "b300", "c300"], ] } my @f = Acme::Foo->get("b like" => "%2%"); is(scalar(@f), 1, "got one object with a like-clause"); is($f[0]->id, 200, "it is correct"); note("*** class 2: in-clause ***"); class Acme::Bar { has => [ a => { is => 'Text' }, b => { is => 'Text' }, c => { is => 'Text' }, foo => { is => "Acme::Foo", id_by => "foo_id" }, ] }; sub Acme::Bar::__load__ { return [qw/id a b c foo_id/], [ [10, "a100", "b100", "c100", 100], [20, "a200", "b200", "c200", 200], [30, "a300", "b300", "c300", 300], ] } my @b = Acme::Bar->get("c" => ['c200', 'c300']); is(scalar(@b), 2, "got two objects with an in-clause"); is($b[0]->id, 20, "first is correct"); is($b[1]->id, 30, "second is correct"); note("*** in-memory joins ***"); my @b2 = Acme::Bar->get("foo.a" => "a100"); is(scalar(@b2), 1, "got one object with a join to another class"); is($b2[0]->id, 10, "it is the correct object"); is($b2[0]->foo->a, "a100", "value is correct"); 60_get_merge_changed_objs_with_db.t000444023532023421 416512121654172 21623 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; # This tests the scenario where we have several objects in the # DB that fulfills a get() request. But before performing that # get(), we change one of the objects so that it will no longer # match the later get(). &setup_classes_and_db(); my $o = URT::Thing->get(thing_id => 2); $o->name('Fred'); # This shouldn't match the below query anymore my @o = URT::Thing->get(name => 'Bob'); is(scalar(@o), 1, 'Get returned 1 object'); is($o[0]->thing_id, 4, 'its ID is correct'); is($o[0]->name, 'Bob', 'its name is correct'); is($o[0]->data, 'baz', 'its data is correct'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 72_command_name_validation.t000444023532023421 135512121654172 20330 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More; my %tests = ( 'WordWord' => 'word-word', 'Word456Word' => 'word456-word', 'Word456aWord' => 'word456a-word', '456Word' => '456-word', 'Word456' => 'word456', 'WWWord' => 'w-w-word', '456' => '456', ); plan tests => scalar(keys(%tests)); for my $class (keys %tests) { my $self = 'URT::' . $class; UR::Object::Type->define( class_name => $self, is => 'Command', ); my $command_name = $self->command_name_brief($class); is($command_name, $tests{$class}, 'command name for class style: ' . $class); } 77_index_undef_value_handling.t000444023532023421 451612121654172 21037 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 9; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table things (thing_id integer, name varchar)"), 'Created things table'); ok( $dbh->do("create table thing_params ( param_id integer, name varchar, value varchar, thing_id integer REFERENCES things(thing_id))"), 'Created params table'); # Bob has the color green, Fred has tracking_number 12345 $dbh->do("insert into things (thing_id, name) values (99, 'Bob')"); $dbh->do("Insert into things (thing_id, name) values (100, 'Fred')"); $dbh->do("Insert into thing_params (param_id, thing_id, name,value) values (1, 99, 'color', 'green')"); $dbh->do("Insert into thing_params (param_id, thing_id, name,value) values (2, 100, 'tracking_number', '12345')"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ name => { is => 'String' }, params => { is => 'URT::ThingParam', is_many => 1, reverse_as => 'thing' }, color => { is => 'String', via => 'params', to => 'value', where => [ name => 'color' ] }, tracking_number => { is => 'String', via => 'params', to => 'value', where => [ name => 'tracking_number' ] }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ); UR::Object::Type->define( class_name => 'URT::ThingParam', id_by => 'param_id', has => [ name => { is => 'String' }, value => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing_params', ); my $thing = URT::Thing->get(color => undef); ok($thing, 'Got thing with no color'); is($thing->name, 'Fred', 'It was the right thing'); my $new_thing = URT::Thing->create(name => 'Joe'); ok($new_thing, 'Created a new object with no color defined'); my $same_thing = URT::Thing->get(name => 'Joe', color => undef); ok($same_thing, 'Got it back by specifying color => undef'); is($new_thing, $same_thing, 'and it was the same object'); 99_transaction-observers.t000444023532023421 1767212121654172 20077 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t use strict; use warnings; use UR; use IO::File; use Test::More tests => 57; UR::Object::Type->define( class_name => 'Circle', has => [ radius => { is => 'Number', default_value => 1, }, ], ); sub add_test_observer { my ($aspect, $context, $observer_ran_ref) = @_; $$observer_ran_ref = 0; my $observer; my $callback; $callback = sub { $$observer_ran_ref = 1; }; $observer = $context->add_observer( aspect => $aspect, callback => $callback, ); unless ($observer) { die "Failed to add $aspect observer!"; } return $observer; } # Create a Circle my $circle = Circle->create(); ok($circle->isa('Circle'), 'create a circle'); ok($circle->radius == 1, 'default radius is 1'); # Verify Transaction Rollback Removes Observer and its Subscription # making sure if someone tries to catch their observer's delete that it runs # before the observer's self-created delete subscription { my $ran_observer_observer = 0; my $circle_trans = UR::Context::Transaction->begin(); ok($circle_trans, 'begin transaction'); my $ran_circle_radius_observer = 0; my $circle_obs = $circle->add_observer( aspect => 'radius', callback => sub { $ran_circle_radius_observer = 1; }, ); my $circle_obs_id = $circle_obs->id; my $ran_circle_obs_delete_obs = 0; my $subscription = $circle_obs->class->create_subscription( id => $circle_obs->id, method => 'delete', callback => sub { $ran_circle_obs_delete_obs = 1; }, note => "$circle_obs", ); my $observer_observer = UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $subscription->[1]); ok($circle_obs->isa('UR::Observer'), 'added an observer on the circle'); is(UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'), $circle_obs, 'Can get the observer on the circle with get()'); my $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id}; ok($circle_sub, 'adding observer inserted a callback into the Context data structure for callbacks'); is(UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs->id, aspect => 'delete'), $observer_observer, 'Can get the observer on the original observer deletion with get()'); ok($circle_trans->rollback(), 'rolled back transaction'); ok($ran_circle_obs_delete_obs == 0, 'rollback did not run the delete observer'); # because it's creation was undone before the radius observer was deleted $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id}; ok(!$circle_sub, 'rolling back transaction (and with it the observer) removed the subscription'); ok($circle_obs->isa('UR::DeletedRef'), 'radius observer is now a DeletedRef'); ok(! UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'), 'get() no longer returns the circle observer'); ok(! UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs_id, aspect => 'delete'), 'get() no longer returns the observer observer'); $ran_circle_obs_delete_obs = 0; $circle->radius(1); is($ran_circle_obs_delete_obs, 0, 'The circle radius observer did not run'); }; # Verify Transaction Rollback Observer Runs { $circle->radius(3); ok($circle->radius == 3, "original radius is three"); my $transaction = UR::Context::Transaction->begin(); my $observer_ran = 0; add_test_observer('rollback', $transaction, \$observer_ran); my $sub = $UR::Context::all_change_subscriptions->{'UR::Context::Transaction'}->{rollback}->{$transaction->id}; ok($sub, 'adding observer also create change subscription'); ok($transaction->isa('UR::Context::Transaction'), "created first transaction (to test rollback observer)"); ok(!$observer_ran, "observer rollback flag reset to 0"); $circle->radius(5); ok($circle->radius == 5, "in transaction (rollback test), radius is five"); ok($transaction->rollback(), "ran transaction rollback"); ok($observer_ran, "rollback observer ran successfully"); ok($circle->radius == 3, "after rollback, radius is three"); }; # Verify Transaction Commit Observer Runs { $circle->radius(4); ok($circle->radius == 4, "original radius (commit test) is four"); my $transaction = UR::Context::Transaction->begin(); my $observer_ran = 0; add_test_observer('commit', $transaction, \$observer_ran); ok($transaction->isa('UR::Context::Transaction'), "created second transaction (to test commit observer)"); ok(!$observer_ran, "observer rollback flag reset to 0"); $circle->radius(6); ok($circle->radius == 6, "in transaction (commit test), radius is six"); ok($transaction->commit(), "ran transaction commit"); ok($observer_ran, "commit observer ran successfully"); ok($circle->radius == 6, "after commit, radius is six"); # Trying to Rollback a Committed Transaction Fails ok($transaction->state eq 'committed', "transaction is already committed"); my $rv= eval {$transaction->rollback()} || 0; ok($rv == 0, "properly failed transaction rollback for already committed transaction"); }; # Test Nested Transactions { $circle->radius(3); ok($circle->radius == 3, "original radius is 3"); my $outer_transaction = UR::Context::Transaction->begin(); my $outer_observer_ran = 0; add_test_observer('rollback', $outer_transaction, \$outer_observer_ran); ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction"); ok(!$outer_observer_ran, "outer observer flag reset to 0"); $circle->radius(5); ok($circle->radius == 5, "in outer transaction, radius is 5"); my $inner_transaction = UR::Context::Transaction->begin(); my $inner_observer_ran = 0; add_test_observer('rollback', $inner_transaction, \$inner_observer_ran); ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction"); ok(!$inner_observer_ran, "inner observer flag reset to 0"); $circle->radius(7); ok($circle->radius == 7, "in inner transaction, radius is 7"); ok($inner_transaction->rollback(), "ran inner transaction rollback"); ok($inner_observer_ran, "inner transaction observer ran successfully"); ok($circle->radius == 5, "after inner transaction rollback, radius is 5"); ok($outer_transaction->rollback(), "ran transaction rollback"); ok($outer_observer_ran, "outer transaction observer ran successfully"); ok($circle->radius == 3, "after rollback, radius is 3"); }; # testing inner commit { $circle->radius(4); ok($circle->radius == 4, "original radius is 4"); my $outer_transaction = UR::Context::Transaction->begin(); my $outer_observer_ran = 0; add_test_observer('rollback', $outer_transaction, \$outer_observer_ran); ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction"); ok(!$outer_observer_ran, "outer observer flag reset to 0"); $circle->radius(6); ok($circle->radius == 6, "in outer transaction, radius is 6"); my $inner_transaction = UR::Context::Transaction->begin(); my $inner_observer_ran = 0; add_test_observer('commit', $inner_transaction, \$inner_observer_ran); ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction"); ok(!$inner_observer_ran, "inner observer flag reset to 0"); $circle->radius(8); ok($circle->radius == 8, "in inner transaction, radius is 8"); ok($inner_transaction->commit(), "ran inner transaction commit"); ok($inner_observer_ran, "inner transaction observer ran successfully"); ok($circle->radius == 8, "after inner transaction commit, radius is 8"); ok($outer_transaction->rollback(), "ran transaction rollback"); ok($outer_observer_ran, "outer transaction observer ran successfully"); ok($circle->radius == 4, "after rollback, radius is 4"); }; done_testing(); 1; 94b_flatten_reframe.t000444023532023421 1630712121654172 17027 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 19; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table CAR_ENGINE (engine_id int NOT NULL PRIMARY KEY, car_id integer references CAR(car_id), size number)'), 'created car_engine table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1], is_optional => 1 }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car_engine values (?,?,?)'); foreach my $row ( [100, 1, 350], [ 200, 2, 400], [300, 3, 428], [400, 4, 429], [500, 5, 289] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'created a subscription for query'); #$DB::single = 1; note("***** FLATTEN AND *****"); my $bx0 = URT::Person->define_boolexpr( 'is_cool' => 1, 'primary_car_color' => 'red', 'primary_car.engine.size' => 428, ); my $bx0f = $bx0->flatten(); my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'cars-primary_car.color' => 'red', 'cars-primary_car.engine.size' => 428, 'cars-primary_car?.is_primary true' => 1, ); is($bx0f->normalize, $bx1->normalize, "flattening works correctly"); note("***** REFRAME AND *****"); my $bx1r1 = $bx1->reframe('primary_car'); my $bx2 = URT::Car->define_boolexpr( 'owner.is_cool' => 1, 'color' => 'red', 'engine.size' => 428, 'is_primary true' => 1, ); is($bx1r1->normalize, $bx2->normalize, "reframe works for a one-step property embedding via/to/where"); my $bx1r2 = $bx1->reframe('primary_car.engine'); my $bx3 = URT::Car::Engine->define_boolexpr( 'car.owner.is_cool' => 1, 'car.color' => 'red', 'size' => 428, 'car.is_primary true' => 1, ); is($bx1r2->normalize->id, $bx3->normalize->id, "reframe works on a two-step chain with the first embedding via/to/where"); my $bx33 = URT::Person->define_boolexpr( 'primary_car.color' => 'red', 'is_cool true' => 1, ); my $bx33r = $bx33->reframe('primary_car'); my $bx33re = URT::Car->define_boolexpr( 'color' => 'red', 'owner.is_cool true' => 1, 'is_primary true' => 1, ); note("***** FLATTEN OR *****"); my $bx4 = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['primary_car.color' => 'red'], ] ); ok($bx4, "created an 'or' boolexpr"); my $bx4f = $bx4->flatten; ok($bx4f, "flattened an OR bx"); my $bx4fe = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['cars-primary_car.color' => 'red', 'cars-primary_car?.is_primary true' => 1], ] ); ok($bx4fe, "defined what we expect for a flattned OR rule"); is($bx4f->id, $bx4fe->id, "the flattened OR rule matches expectations"); note("***** REFRAME OR *****"); my $bx4r = $bx4->reframe('primary_car'); ok($bx4r, "reframed OR expression"); my $bx4re = URT::Car->define_boolexpr( -or => [ ['owner.is_cool' => 1], ['color' => 'red', 'is_primary true' => 1], ], ); ok($bx4re, "created expected reframe expression"); is($bx4r->id, $bx4re->id, "reframed expression matches the expected expression"); note("***** FLATTEN WITH ORDER/GROUP *****"); my $bx5 = URT::Person->define_boolexpr( 'is_cool true' => 1, 'primary_car_color' => 'red', '-group_by' => ['is_cool','primary_car_color','name'], '-order_by' => ['is_cool','primary_car_color'], ); my $bx5r = $bx5->reframe('primary_car'); my $bx5re = URT::Car->define_boolexpr( 'owner.is_cool true' => 1, 'color' => 'red', '-group_by' => ['owner.is_cool','color','owner.name'], '-order_by' => ['owner.is_cool','color'], 'is_primary true' => 1, ); is($bx5r->id, $bx5re->id, "reframe works on -order_by"); note("$bx5re\n$bx5r\n"); note("***** FLATTEN AROUND JOIN TO OPTIONAL WITH ON CLAUSE *****"); my $bx6 = URT::Person->define_boolexpr( is_cool => 1, -hints => ['primary_car'] ); my $bx6f = $bx6->flatten; __END__ #$DB::single = 1; #$ENV{UR_DBI_MONITOR_SQL} = 1; my @p6f = URT::Person->get($bx6f); my @p6 = URT::Person->get($bx6); is("@p6f", "@p6", "got the same objects back after flattening around an optional relationship"); my @p6b = URT::Person->get($bx6f); is("@p6", "@p6b", "a repeate of the original query gets the same answer from the context"); my @p6fb = URT::Person->get($bx6f); is("@p6f", "@p6fb", "a repeate of the flattened query gets the same answer from the context"); 001_util_on_destroy.t000444023532023421 203712121654172 16770 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use File::Basename; use Test::More; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; sub dummy { eval{} }; plan tests => 7; use UR; my $x = 1; my $sentry = UR::Util::on_destroy { $x = 2; dummy() }; is($x, 1, "value is not updated when the sentry has not been destroyed"); $sentry = undef; is($x, 2, "value is updated when the sentry has been destroyed"); $x = 1; sub foo { my $sentry = UR::Util::on_destroy { $x = 3; dummy(); }; is($x, 1, "value is not updated while the sentry is still in scope"); } foo(); is($x, 3, "value is updated after the sentry goes out of scope"); $x = 1; sub bar { my $sentry = UR::Util::on_destroy { $x = 4; dummy(); }; is($x, 1, "value is updated while the sentry is still in scope"); die "ouch"; } eval { bar(); }; my $exception = $@; is($x, 4, "value is updated after the sentry goes out of scope during thrown exception"); ok($@, "exception is passed through even thogh the sentry does an eval internally: $@"); 81_crud_custom_columnnames.t000444023532023421 655312121654172 20435 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; &create_tables_and_classes(); my $p1 = URT::Product->get(1); ok(!$p1, 'Get by non-existent ID correctly returns nothing'); my $p2 = URT::Product->create(id => 1, name => 'jet pack', genius => 6, manufacturer_name => 'Lockheed Martin',cost => 5); ok($p2, 'Create a new Product with the same ID'); $p1 = URT::Product->get(1); ok($p1, 'Get with the same ID returns something, now'); is($p1->id, 1, 'ID is correct'); is($p1->name, 'jet pack', 'name is correct'); is($p1->genius, 6, 'name is correct'); is($p1->manufacturer_name, 'Lockheed Martin', 'name is correct'); my $p3 = URT::Product->get(100); ok($p3, 'Retrieve product with ID 100'); is($p3->cost, 100, 'Its cost is 100'); is($p3->genius, 1, 'Its genius is 1'); ok($p3->cost(5000), 'Change cost to 5000'); ok($p3->genius(99), 'Change genius to 99'); my $p4 = URT::Product->get(101); ok($p4, 'Retrieve product with ID 101'); ok($p4->delete, 'Delete it'); ok(UR::Context->commit(), 'Commit'); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; my $sth = $dbh->prepare('select * from product'); $sth->execute(); my %products_by_id; while (my $row = $sth->fetchrow_hashref) { my %copy = %$row; $products_by_id{$copy{'product_prod_id'}} = \%copy; } $sth->finish; is(scalar(keys %products_by_id), 2, 'There were 2 products in the database'); my $expected = { 1 => { product_prod_id => 1, product_name => 'jet pack', product_genius => 6, product_mfg_name => 'Lockheed Martin', cost => 5, }, 100 => { product_prod_id => 100, product_name => 'Something to update', product_genius => 99, product_mfg_name => 'Acme', cost => 5000, }, }; is_deeply(\%products_by_id, $expected, 'Data in DB is as expected'); #note(Data::Dumper::Dumper(\%products_by_id)); sub create_tables_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PRODUCT ( product_prod_id int NOT NULL PRIMARY KEY, product_name varchar, product_genius integer, product_mfg_name varchar, cost integer)'), 'created product table'); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', id_by => [ prod_id => { is => 'NUMBER', sql => 'product_prod_id' }, ], has => [ name => { is => 'STRING', sql => 'product_name' }, genius => { is => 'NUMBER', sql => 'product_genius' }, manufacturer_name => { is => 'STRING', sql => 'product_mfg_name' }, cost => { is => 'NUMBER' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); ok($dbh->do("insert into product (product_prod_id,product_name,product_genius,product_mfg_name,cost) values (100,'Something to update',1,'Acme',100)"), 'Inserted item 1'); ok($dbh->do("insert into product (product_prod_id,product_name,product_genius,product_mfg_name,cost) values (101,'Something to delete',1,'Acme',200)"), 'Inserted item 101'); $dbh->commit(); } 03c_rule_values.t000444023532023421 753612121654172 16172 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl # Test handling of rules and their values with different kinds # params. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 21; use Data::Dumper; use IO::Handle; class URT::RelatedItem { id_by => 'ritem_id', has => [ ritem_property => { is => 'String' }, ], }; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, score => { is => 'Integer' }, ritem => { is => 'URT::RelatedItem', id_by => 'ritem_id' }, ] }; my($r, @values, $n, $expected,$fh); $r = URT::Item->define_boolexpr(name => ['Bob'], foo => undef, -hints => ['ritem']); ok($r, 'Created boolexpr'); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 2, 'Got back 2 values from rule'); $expected = [['Bob'], undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # foo, name @values = $n->values(); $expected = [undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); $fh = IO::Handle->new(); $r = URT::Item->define_boolexpr(name => ['Bob'], fh => $fh, foo => undef); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [['Bob'], $fh, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # fh, foo, name @values = $n->values(); $expected = [$fh, undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); $r = URT::Item->define_boolexpr(name => ['Bob'], fh => $fh, foo => undef, -hints => ['ritem']); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [['Bob'], $fh, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # -hints, fh, foo, name @values = $n->values(); $expected = [$fh, undef, ['Bob']]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); my @p = (name => [$fh], score => 1, foo => undef, -hints => ['ritem']); $r = URT::Item->define_boolexpr(@p); my @p2 = $r->params_list(); #is("@p","@p2",'params return correctly with hint'); is_deeply(\@p,\@p2, "match deeply"); # These values are in the same order as the original rule definition @values = $r->values(); is(scalar(@values), 3, 'Got back 3 values from rule'); $expected = [[$fh], 1, undef]; is_deeply(\@values, $expected, "Rule's values are correct"); is($values[0][0], $p[1][0], 'object is preserved within the arrayref of references'); $n = $r->normalize; ok($n, 'Normalized rule'); # Normalized values come back alpha sorted by their param's name # foo, name, score @values = $n->values(); $expected = [undef, [$fh], 1]; is_deeply(\@values, $expected, "Normalized rule's values are correct"); # Check that duplicate values in an in-clause are handled correctly my $rule = URT::Item->define_boolexpr(name => ['Bob', 'Bob', 'Rob', 'Rob', 'Joe', 'Foo']); ok($rule, 'rule with duplicate values created'); my $values = $rule->value_for('name'); my @expected = ('Bob', 'Foo', 'Joe','Rob'); is_deeply($values, \@expected, 'duplicates were filtered out correctly'); 57_order_by_merge_new_objects.t000444023532023421 474712121654172 21061 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; # There are 2 items in the DB and one newly created object in the cache # that will satisfy the get(). Make sure they're all returned and in # the order asked for &setup_classes_and_db(); my $newobj = URT::Thing->create(thing_id => -1, name => 'Alan', data => 'baaa'); # The default order is by thing_id which would return them in the # order 'Mike', 'Fred. my @o = URT::Thing->get('data like' => 'ba%', -order => ['name']); is(scalar(@o), 3, 'Got 3 objects with data like ba%'); is($o[0], $newobj, 'First object is the newly created object'); is($o[1]->id, 4, 'Second object id is 4'); is($o[1]->name, 'Bobby', 'Second object name is Bobby'); is($o[1]->data, 'baz', 'Second object data is baz'); is($o[2]->id, 1, 'Third object id is 1'); is($o[2]->name, 'Joe', 'Third object name is Joe'); is($o[2]->data, 'bar', 'Third object data is bar'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [1, 'Joe', 'bar'], [2, 'Bob', 'foo'], [3, 'Fred', 'quux'], [4, 'Bobby', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 71_ur_value.t000444023532023421 2330312121654172 15336 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 86; require File::Temp; my $s1 = UR::Value::Text->get('hi there'); ok($s1, 'Got an object for string "hi there"'); is($s1->id, 'hi there', 'It has the right id'); my $s2 = UR::Value::Text->get('hi there'); ok($s2, 'Got another object for the same string'); is($s1,$s2, 'They are the same object'); my $s3 = UR::Value::Text->get('something else'); ok($s3, 'Got an object for a different string'); isnt($s1,$s3, 'They are different objects'); my $s4 = UR::Value::Text->get('0'); ok(defined($s4), 'Got an object for the string "0"'); # Note that $s4 stringifies to "0" which is boolean false is($s4->id, '0', 'The ID is correct'); is($s4, '0', 'It stringifies correctly'); my $text = UR::Value::Text->get('metagenomic composition 16s is awesome'); ok($text, 'Got an object for string "metagenomic composition 16s is awesome"'); is($text->id, 'metagenomic composition 16s is awesome', 'Id is correct'); my $capitalized = $text->capitalize; isa_ok($capitalized, 'UR::Value::Text'); is($capitalized->id, 'Metagenomic Composition 16s Is Awesome', 'Capitalized for is "Metagenomic Composition 16s Is Awesome"'); my $camel = $text->to_camel; isa_ok($camel, 'UR::Value::Text'); is($camel->id, 'MetagenomicComposition16sIsAwesome', 'Text To camel case for is "MetagenomicComposition16sIsAwesome"'); my $lemac = $camel->to_lemac; isa_ok($lemac, 'UR::Value::Text'); is($lemac->id, 'metagenomic composition 16s is awesome', 'Camel case to text for is "MetagenomicComposition16sIsAwesome"'); is($lemac, $text, 'Got the same UR::Value::Text object back for camel case to text'); ok(!$text->to_hash, 'Failed to convert text object "' . $text->id . '"to a hash when does not start with a dash (-)'); my $text_for_text_to_hash = '-aa foo -b1b -1 bar --c22 baz baz -ddd -11 -eee -f -g22g text -1111 --h_h 44 --i-i -5 -j-----j -5 -6 hello -k -l_l-l g a p -m'; my $text_to_hash = UR::Value::Text->get($text_for_text_to_hash); ok($text_to_hash, 'Got object for param text'); my $hash = $text_to_hash->to_hash; ok($hash, 'Got hash for text'); is_deeply($hash->id, { aa => 'foo', b1b => '-1 bar', c22 => 'baz baz', ddd => -11, eee => '', f => '', g22g => 'text -1111', h_h => 44, 'i-i' => -5, 'j-----j' => '-5 -6 hello', k => '', 'l_l-l' => 'g a p', m => '', }, 'Text to hash id is correct'); is($hash->__display_name__, "aa => 'foo',b1b => '-1 bar',c22 => 'baz baz',ddd => '-11',eee => '',f => '',g22g => 'text -1111',h_h => '44',i-i => '-5',j-----j => '-5 -6 hello',k => '',l_l-l => 'g a p',m => ''", 'Hash display name'); my $hash_to_text = $hash->to_text; ok($hash_to_text, 'Got hash to text'); is($hash_to_text, '-aa foo -b1b -1 bar -c22 baz baz -ddd -11 -eee -f -g22g text -1111 -h_h 44 -i-i -5 -j-----j -5 -6 hello -k -l_l-l g a p -m', 'Hash to text is correct'); my $s1_refaddr = Scalar::Util::refaddr($s1); ok($s1->unload(), 'Unload the original string object'); isa_ok($s1, 'UR::DeletedRef'); isa_ok($s2, 'UR::DeletedRef'); $s1 = UR::Value::Text->get('hi there'); ok($s1, 're-get the original string object'); is($s1->id, 'hi there', 'It has the right id'); isnt(Scalar::Util::refaddr($s1), $s1_refaddr, 'It is not the original object reference'); UR::Object::Type->define( class_name => 'Test::Value', is => 'UR::Value', id_by => [ string => { is => 'Text' } ] ); eval { Test::Value->get() }; like($@, qr/Can't load an infinite set of Test::Value/, 'Getting infinite set of Test::Values threw an exception'); my $x1 = Test::Value->get('xyz'); ok($x1,"get('xyz') returned on first call"); my $x2 = Test::Value->get('xyz'); ok($x2,"get('xyz') returned on second call"); is($x1, $x2, 'They were the same object'); my $a1 = Test::Value->get(string => 'abc'); ok($a1,"get(string => 'abc') returned on first call"); my $a2 = Test::Value->get(string => 'abc'); ok($a2,"get(string => 'abc') returned on second call"); is($a1, $a2, 'They were the same object'); my $n1 = Test::Value->get('123'); ok($n1, "get('123') returned on first call"); my $n2 = Test::Value->get(string => '123'); ok($n2,"get(string => '123') returned on second call"); is($n1, $n2, 'They were the same object'); my @o = Test::Value->get(['xyz','abc','123','456']); is(scalar(@o), 4, 'Got 4 Test::Values in a single get()'); is_deeply([ map { $_->id} @o], ['123','456','abc','xyz'], 'Values were returned in ID order'); my %o = map { $_->id => $_ } @o; is($o{'123'}, $n1, "Object with id '123' is the same as the one from earlier"); is($o{'abc'}, $a1, "Object with id 'abc' is the same as the one from earlier"); is($o{'xyz'}, $x1, "Object with id 'xyz' is the same as the one from earlier"); is($o{'456'}->string, '456', 'The 4th value in the last get() constructed the correct object'); UR::Object::Type->define( class_name => 'Test::Value2', is => 'UR::Value', id_by => [ string1 => { is => 'Text' }, string2 => { is => 'Text' }, ], has => [ other_prop => { is => 'Text' }, ], ); eval { Test::Value2->get(string1 => 'abc') }; like($@, qr/Can't load an infinite set of Test::Value2/, 'Getting infinite set of Test::Value2s threw an exception'); $a1 = Test::Value2->get(string1 => 'qwe', string2 => undef); ok($a1, "get(string1 => 'qwe', string2 => undef) worked"); $a2 = Test::Value2->get(id => 'qwe'); ok($a2, "get(id => 'qwe') worked"); is($a1, $a2, 'They were the same object'); $a1 = Test::Value2->get(string1 => 'abc', string2 => 'def'); ok($a1, 'get() with both ID properties worked'); my $sep = Test::Value2->__meta__->_resolve_composite_id_separator; $a2 = Test::Value2->get('abc' . $sep . 'def'); ok($a2, 'get() with the composite ID property worked'); is($a1, $a2, 'They are the same object'); is($a1->other_prop, undef, 'The non-id property is undefined'); $x1 = Test::Value2->get(string1 => 'xyz', string2 => 'xyz', other_prop => 'hi there'); ok($x1, 'get() including a non-id property worked'); is($x1->other_prop, 'hi there', 'The non-id property has the right value'); TODO: { local $TODO = "Can't normalize a composite id in-clause rule"; # This isn't working properly because of a shortcoming in BoolExpr normalization. It ends up making # a rule like id => [abc,xyz], when we really want something like # ( string1 => 'abc' and string2 => 'abc) or ( string1 => 'xyz' and string2 => 'xyz') local $SIG{'__WARN__'} = sub {}; # Suppress warnings about is_unique during boolexpr construction @o = Test::Value2->get(['xyz'.$sep.'xyz', 'abc'.$sep.'abc']); is(scalar(@o), 2, 'get() with 2 composite IDs worked'); } { local $SIG{'__WARN__'} = sub {}; # Suppress warnings about is_unique during boolexpr construction eval { Test::Value2->get(id => ['xyz'.$sep.'xyz', 'abc'.$sep.'abc'], other_prop => 'somethign else') }; like($@, qr/Cannot load class Test::Value2 via UR::DataSource::Default when 'id' is a listref and non-id properties appear in the rule/, 'Getting with multiple IDs and including non-id properites threw an exception'); } do { do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); isa_ok($path, 'UR::Value::FilesystemPath', 'path'); is($path, $pathname, 'comparing path object to string works'); }; do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); $path .= 'a'; $pathname .= 'a'; isa_ok($path, 'UR::Value::FilesystemPath', 'after concatenation path still'); is($path, $pathname, 'string concatenation works'); }; do { my $pathname = 'foo'; my $path = UR::Value::FilesystemPath->get($pathname); like($path, qr/foo/, 'matching works'); }; }; do { # file test "operators" my $temp_file = File::Temp->new(); ok(-f $temp_file, 'created temp_file'); my $temp_dir = File::Temp->newdir(); ok(-d $temp_dir, 'created temp_dir'); my $temp_filename = $temp_file->filename; my $temp_dirname = $temp_dir->dirname; my $symlink_filename_a = $temp_dirname . '/symlink_a'; symlink($temp_filename, $symlink_filename_a); ok(-l $symlink_filename_a, 'created symlink'); do { # file my $path = UR::Value::FilePath->get($temp_filename); isa_ok($path, 'UR::Value::FilesystemPath', 'file path'); is($path->exists, 1, 'file path exists'); is($path->is_dir, '', 'file path is not a dir'); is($path->is_file, 1, 'file path is a file'); is($path->is_symlink, '', 'file path is not a symlink'); is($path->size, 0, 'file path size is zero'); system("echo hello > $path"); isnt($path->size, 0, "file path size isn't zero"); is($path->line_count, 1, 'file path has one line'); }; do { # dir my $path = UR::Value::FilesystemPath->get($temp_dirname); isa_ok($path, 'UR::Value::FilesystemPath', 'dir path'); is($path->exists, 1, 'dir path exists'); is($path->is_dir, 1, 'dir path is a dir'); is($path->is_file, '', 'dir path is not a file'); is($path->is_symlink, '', 'dir path is not a symlink'); }; do { # symlink my $path = UR::Value::FilesystemPath->get($symlink_filename_a); isa_ok($path, 'UR::Value::FilesystemPath', 'symlink path'); is($path->exists, 1, ' symlink path exists'); is($path->is_dir, '', ' symlink path is not a dir'); is($path->is_file, 1, ' symlink path is a file'); is($path->is_symlink, 1, ' symlink path is a symlink'); my $symlink_filename_b = "$temp_dirname/symlink_b"; symlink($path, $symlink_filename_b); ok(-l $symlink_filename_b, 'created symlink_b (from an object)'); }; }; 59_get_merge_new_objs_with_db.t000444023532023421 463112121654172 21031 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 5; # This tests the scenario where the Context's loading iterator must # merge objects fulfilling a get() request between objects in # the cache and objects loaded from a DB &setup_classes_and_db(); URT::Thing->create(thing_id => 1, name => 'Bob', data => '1234'); URT::Thing->create(thing_id => 3, name => 'Bob', data => '5678'); my @o = URT::Thing->get(name => 'Bob'); # 2 objects in the DB plus 2 more that we created is(scalar(@o), 4, 'Get returned 4 objects'); my @expected = ( { thing_id => 1, name => 'Bob', data => '1234' }, { thing_id => 2, name => 'Bob', data => 'foo' }, { thing_id => 3, name => 'Bob', data => '5678' }, { thing_id => 4, name => 'Bob', data => 'baz' }, ); my @got = map { { thing_id => $_->thing_id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Data returned is as expected'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'] )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 38_join_across_data_sources.t000444023532023421 2201512121654172 20563 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 50; # FIXME - This tests the simple case of a single indirect property. # Need to add a test for a doubly-indirect property crossing 2 data # sources, and a test where the numeric order of things is differen # than the alphabetic order use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $tmp_path = "/tmp/ur_testsuite$$"; ok(mkdir($tmp_path), "mkdir temp dir"); our $DB_FILE_1 = "$tmp_path/ur_testsuite_db1_$$.sqlite"; our $DB_FILE_2 = "$tmp_path/ur_testsuite_db2_$$.sqlite"; END { &clean_tmp_dir($tmp_path); } &create_data_sources($tmp_path); &populate_databases($tmp_path); &create_test_classes($tmp_path); # Set up subscriptions to count queries and loads my($db1_query_count, $primary_load_count, $db2_query_count, $related_load_count); sub reset_counts { ($db1_query_count, $primary_load_count, $db2_query_count, $related_load_count) = (0,0,0,0); } ok(URT::38Primary->create_subscription( method => 'load', callback => sub {$primary_load_count++}), 'Created a subscription for URT::38Primary load'); ok(URT::38Related->create_subscription( method => 'load', callback => sub {$related_load_count++}), 'Created a subscription for URT::38Related load'); ok(URT::DataSource::SomeSQLite1->create_subscription( method => 'query', callback => sub {$db1_query_count++}), 'Created a subscription for SomeSQLite1 query'); ok(URT::DataSource::SomeSQLite2->create_subscription( method => 'query', callback => sub {$db2_query_count++}), 'Created a subscription for SomeSQLite2 query'); &reset_counts(); my @o = URT::38Primary->get(related_value => '1'); is(scalar(@o), 1, "contained_value => 1 returns one Primary object"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 1, "Loaded 1 Related object"); &reset_counts(); @o = URT::38Primary->get(primary_value => 'Two', related_value => '2'); is(scalar(@o), 1, "container_value => 'Two',contained_value=>2 returns one Primary object"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 1, "Loaded 1 Related object"); &reset_counts(); @o = URT::38Primary->get(related_value => '2'); is(scalar(@o), 2, "contained_value => 2 returns two Primary objects"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 1, "Loaded 1 Primary object"); # FIXME - This next one should really be 0, as the resulting query against db2 is exactly the same as # the prior get() above. The problem is that the cross-datasource join logic is # functioning at the database level, not the object level. So there's no good way of # knowing that we've already done that query. is($db2_query_count, 1, "Correctly didn't query db 2 (same as previous query)"); is($related_load_count, 0, "Correctly loaded 0 Related objects (they're cached)"); &reset_counts(); @o = URT::38Primary->get(related_value => '3'); is(scalar(@o), 0, "contained_value => 3 correctly returns no Primary objects"); is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); # Note - it kind of doesn't make sense that we do a query against db2, and that query does # match one item in there. UR doesn't go ahead and load it because the query against the # primary DB returns no rows, so there's nothing to 'join' against, and no rows from db2's # query are fetched is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "Correctly loaded 0 Related object"); &reset_counts(); @o = URT::38Primary->get(related_value => '4'); is(scalar(@o), 0, "contained_value => 4 correctly returns no Primary objects"); # Note - same thing here, the primary query fetches 1 row, but doesn't successfully # join to any rows in the secondary query, so no objects get loaded. is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "correctly loaded 0 Related objects"); &reset_counts(); @o = URT::38Related->get(related_value => 2, primary_values => 'Two'); is(scalar(@o), 1, 'URT::Related->get(primary_value => 2) returned 1 object'); # This actually ends up being 4 because of the way the Indexes get created. Don't think it's # useful to test it #is($db1_query_count, 1, "Queried db 1 one time"); is($primary_load_count, 0, "correctly loaded 0 Primary objects"); is($db2_query_count, 1, "Queried db 2 one time"); is($related_load_count, 0, "correctly loaded 0 Related objects"); sub create_data_sources { IO::File->new($DB_FILE_1, 'w')->close(); class URT::DataSource::SomeSQLite1 { is => 'UR::DataSource::SQLite', }; sub URT::DataSource::SomeSQLite1::server { $DB_FILE_1 }; IO::File->new($DB_FILE_2, 'w')->close(); class URT::DataSource::SomeSQLite2 { is => 'UR::DataSource::SQLite', }; sub URT::DataSource::SomeSQLite2::server { $DB_FILE_2 }; } sub create_test_classes { return; my $tmp_path = shift; # We have to write them out as files instead of calling UR::Object::Type->define() # because each class refers to the other unshift(@INC, $tmp_path); mkdir("$tmp_path/URT") || die "Can't create dir $tmp_path/URT"; my $f = IO::File->new("$tmp_path/URT/Related.pm",'>'); $f->print(q( use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::Object::Type->define( class_name => 'URT::Related', id_by => [ related_id => { is => 'Integer' }, ], has => [ related_value => { is => 'String' }, primary_objects => { is => 'URT::Primary', reverse_as => 'related_object', is_many => 1 }, primary_values => { vis => 'primary_object', to => 'primary_value', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite2', table_name => 'related', ) 1; )); $f->close(); $f = IO::File->new("$tmp_path/URT/Primary.pm",'>'); $f->print(q( use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::Object::Type->define( class_name => 'URT::Primary', id_by => [ primary_id => { is => 'Integer' }, ], has => [ primary_value => { is => 'String' }, related_id => { is => 'Integer'}, related_object => { is => 'URT::Related', id_by => 'related_id' }, related_value => { via => 'related_object', to => 'related_value' }, ], data_source => 'URT::DataSource::SomeSQLite1', table_name => 'primary_table', ); 1; )); $f->close(); } sub populate_databases { my $dbh = URT::DataSource::SomeSQLite1->get_default_handle(); ok($dbh, 'Got db handle for URT::DataSource::SomeSQLite1'); ok($dbh->do("create table primary_table (primary_id integer PRIMARY KEY, primary_value varchar, rel_id integer)"), "create primary table"); # This one will match one item in related ok($dbh->do("insert into primary_table values (1, 'One', 1)"), "insert row 1 into primary"); # these two things will match one in related ok($dbh->do("insert into primary_table values (2, 'Two', 2)"), "insert row 2 into primary"); ok($dbh->do("insert into primary_table values (3, 'Three', 2)"), "insert row 3 into primary"); # Nothing here matches related's 3 # This will match nothing in related ok($dbh->do("insert into primary_table values (4, 'Four', 4)"), "insert row 4 into primary"); ok($dbh->commit(), "Commit SomeSQLite1 DB"); $dbh = URT::DataSource::SomeSQLite2->get_default_handle(); ok($dbh, 'Got db handle for URT::DataSource::SomeSQLite2'); ok($dbh->do("create table related (related_id integer PRIMARY KEY, related_value varchar)"), "crate related table"); ok($dbh->do("insert into related values (1, '1')"), "insert row 1 into related"); ok($dbh->do("insert into related values (2, '2')"), "insert row 2 into related"); ok($dbh->do("insert into related values (3, '3')"), "insert row 4 into related"); ok($dbh->commit(), "Commit SomeSQLite2 DB"); } sub clean_tmp_dir { my $tmp_dir = shift; my $dbh = URT::DataSource::SomeSQLite1->get_default_handle(); $dbh->disconnect(); $dbh = URT::DataSource::SomeSQLite2->get_default_handle(); $dbh->disconnect(); #diag("Cleanup tmp dir"); # These _should_ be the only files in there... ok(unlink($DB_FILE_1), 'Remove sqlite DB 1'); ok(unlink($DB_FILE_2), 'Remove sqlite DB 2'); ok(rmdir($tmp_dir), "Remove tmp dir $tmp_dir"); } 13a_messaging.t000444023532023421 1437612121654172 15640 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use IO::Socket; use Data::Dumper; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use Test::More tests => 828; use UR::Namespace::Command::Old::DiffRewrite; my $c = "UR::Namespace::Command::Old::DiffRewrite"; # The messaging methods print to the filehandle $Command::stderr, which defaults # to STDERR. Redefine it so the messages are printed to a filehandle we # can read from, $stderr_twin, but regular perl diagnostic messages still go # to the real STDERR my $stderr_twin; $UR::ModuleBase::stderr = undef; socketpair($UR::ModuleBase::stderr,$stderr_twin, AF_UNIX, SOCK_STREAM, PF_UNSPEC); $UR::ModuleBase::stderr->autoflush(1); $stderr_twin->blocking(0); my $buffer; for my $type (qw/error warning status/) { my $accessor = $type . "_message"; my $uc_type = uc($type); my $msg_prefix = ($type eq "status" ? "" : "$uc_type: "); my $msg_source_sub = $accessor . '_source'; for my $do_queue ([],[0],[1]) { for my $do_dump ([],[0],[1]) { my $dump_flag = "dump_" . $type . "_messages"; $c->$dump_flag(@$do_dump); my $queue_flag = "queue_" . $type . "_messages"; $c->$queue_flag(@$do_queue); my $list_accessor = $accessor . "s"; is($c->$accessor(), undef , "$type starts unset"); $buffer = $stderr_twin->getline; is($buffer, undef, "no message"); my $cb_register = $type . "_messages_callback"; my $cb_msg_count = 0; my @cb_args; my $callback_sub = sub { @cb_args = @_; $cb_msg_count++;}; ok($c->$cb_register($callback_sub), "can set callback"); is($c->$cb_register(), $callback_sub, 'can get callback'); my $message_line = __LINE__ + 1; # The messaging sub will be called on the next line is($c->$accessor("error1"), "error1", "$type setting works"); $buffer = $stderr_twin->getline; is($buffer, ($c->$dump_flag ? "${msg_prefix}error1\n" : undef), ($c->$dump_flag ? "got message 1" : "no dump") ); my %source_info = $c->$msg_source_sub(); is_deeply(\%source_info, { $accessor => 'error1', $type.'_package' => 'main', $type.'_file' => __FILE__, $type.'_line' => $message_line, $type.'_subroutine' => undef }, # not called from within a sub "$msg_source_sub returns correct info"); is($cb_msg_count, 1, "$type callback fired"); is_deeply( \@cb_args, [$c, "error1"], "$type callback got correct args" ); is($c->$accessor(), "error1", "$type returns"); $buffer = $stderr_twin->getline; is($buffer, undef, "no dump"); is($c->$accessor("error2"), "error2", "$type resetting works"); $buffer = $stderr_twin->getline; is($buffer, ($c->$dump_flag ? "${msg_prefix}error2\n" : undef), ($c->$dump_flag ? "got message 2" : "no dump") ); is($cb_msg_count, 2, "$type callback fired"); is($c->$accessor(), "error2", "$type returns"); is_deeply( \@cb_args, [$c, "error2"], "$type callback got correct args" ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); is($c->$accessor(undef), undef , "undef message sent to $type"); is($cb_msg_count, 3, "$type callback fired"); $buffer = $stderr_twin->getline; is($buffer, undef, 'Setting undef message results in no output'); is($c->$accessor(), undef , "$type still has the previous message"); is_deeply( \@cb_args, [$c, undef], "$type callback got correct args" ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); my $listref_accessor = $list_accessor . "_arrayref"; my $listref = $c->$listref_accessor(); is_deeply( $listref, ($c->$queue_flag ? ['error1','error2'] : []), "$type listref is correct" ); $c->$cb_register(sub { $_[1] .= "foo"}); $c->$accessor("altered"); $buffer = $stderr_twin->getline(); is($buffer, ($c->$dump_flag ? "${msg_prefix}alteredfoo\n" : undef), ($c->$dump_flag ? "got altered message" : "no dump") ); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2","alteredfoo"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); $c->$cb_register(undef); # Unset the callback is($c->$accessor(undef), undef , "undef message sent to $type message"); is($cb_msg_count, 3, "$type callback correctly didn't get fired"); $buffer = $stderr_twin->getline(); is($buffer, undef, 'Setting undef message results in no output'); is_deeply( [$c->$list_accessor], ($c->$queue_flag ? ["error1","error2","alteredfoo"] : []), ($c->$queue_flag ? "$type list is correct" : "$type list is correctly empty") ); if ($c->$queue_flag) { $listref->[2] = "something else"; is_deeply( [$c->$list_accessor], ["error1","error2","something else"], "$type list is correct after changing via the listref" ); @$listref = (); is_deeply( [$c->$list_accessor], [], "$type list cleared out as expected" ); } } } } 1; 74_xsl_view_url_convert.t000444023532023421 153412121654172 17763 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More; BEGIN { eval "use XML::LibXSLT"; if ($@) { plan skip_all => "Cannot load XML::LibXSLT: $@"; } else { plan tests => 11; use_ok('UR::Object::View::Default::Xsl', qw/url_to_type type_to_url/); } } my @ct = qw{ genome/instrument-data Genome::InstrumentData genome Genome genome/foo-bar/baz Genome::FooBar::Baz funky-town FunkyTown funky-town/oklahoma FunkyTown::Oklahoma }; for ( my $i = 0 ; $i + 1 < @ct ; $i += 2 ) { is( url_to_type( $ct[$i] ), $ct[ $i + 1 ], 'url_to_type ' . $ct[$i] ); is( type_to_url( $ct[ $i + 1 ] ), $ct[$i], 'type_to_url ' . $ct[ $i + 1 ] ); } 56b_order_by_calculated_property.t000444023532023421 533012121654172 21573 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; # When a different ordering is requested, make sure a get() that hits # the DB returns items in the same order as one that returns cached objects. # It should be sorted first by the requested key, then by ID my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); # Inserting them purposfully in non-ID order so they'll get returned in non-id # order if the ID column isn't included in the 'order by' clause foreach my $row ( ( [4, 'Bobby', 'abc'], [2, 'Bob', 'abc'], [1, 'Bobert', 'zzz'], [6, 'Bobert', 'infinity'], [5, 'Bobs', 'aaa'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, uc_name => { is => 'String', calculate_from => ['name'], calculate => q( uc($name) ) }, data => { is => 'String' }, uc_data => { is => 'String', calculate_from => ['data'], calculate => q( uc($data) ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['uc_data']); is(scalar(@o), 5, 'Got 2 things with name like Bob% ordered by uc_name'); my @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; my @expected = ( { id => 5, name => 'Bobs', data => 'aaa' }, { id => 2, name => 'Bob', data => 'abc' }, { id => 4, name => 'Bobby', data => 'abc' }, { id => 6, name => 'Bobert', data => 'infinity' }, { id => 1, name => 'Bobert', data => 'zzz' }, ); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Bob%', -order => ['uc_data']); is(scalar(@o), 5, 'Got 2 things with name like Bob% ordered by data'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); 77_sql_undef_value_handling.t000444023532023421 1534612121654172 20552 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; # Test the different ways that SQL's handling of NULL might differ # with the way Perl and UR convert NULL to undef and the various # numeric and string conversions when doing comparisions. We want UR's # object cache to return the same results that a query against the database # would use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 227; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table things (thing_id integer, value integer)"), 'Created things table'); $dbh->do("insert into things (thing_id, value) values (1, NULL)"); $dbh->do("Insert into things (thing_id, value) values (2, NULL)"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ); my @result; # For the equality operator, "value => undef" is converted to SQL as # "value IS NULL", not "value = NULL, so it should return the items foreach my $value ( undef ) { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } TODO: { local $TODO = "empty string and undef in a rule will mean the same thing soonly"; foreach my $value ( '') { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } }; # For other values using the equality operator, it should return nothing foreach my $value ( 0, 1, -1) { operator_returns_object_count('', $value,0); } ## != for non-null values should return both things foreach my $value ( 0, 1, -1) { my @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) loaded 2 items"); @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } # the 'false' operator should return both things, since NULL is false { my @result = URT::Thing->get(value => { operator => 'false', value => '' }); is(scalar(@result), 2, "value is false (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => 'false', value => ''}); is(scalar(@result), 2, "value is false (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) loaded 2 items"); @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } foreach my $operator ( qw( < <= > >= true ) ) { foreach my $value ( undef, 0, "", 1, -1) { operator_returns_object_count($operator,$value,0); last if ($operator eq 'true' or $operator eq 'false'); # true and false don't use the 'value' anyway } } # FIXME - uninitialized warnings here foreach my $operator ( 'like', 'not like' ) { foreach my $value ( undef, '%', '%1', '%1%' ) { operator_returns_object_count($operator, $value, 0) } } # Supress messages about null in-clauses. URT::DataSource::SomeSQLite->warning_messages_callback( sub { my ($self,$msg) = @_; if ($msg =~ m/Null in-clause passed/) { $_[1] = undef; } } ); # 'in' operator # value => [undef] does SQL to include NULL items operator_returns_object_count('in', [undef], 2); operator_returns_object_count('not in', [undef], 0); foreach my $operator ( '', 'in', 'not in' ) { foreach my $value ( [], [1] ) { operator_returns_object_count($operator, $value, 0); } } # 'between' operator foreach my $value ( [undef, undef], [1,1], [0,1], [-1,0], [-1,-1], [undef, 1], [undef, 0], [undef, -1], [1, undef], [0, undef], [-1, undef] ) { operator_returns_object_count('between', $value, 0); } sub operator_returns_object_count { my($operator,$value,$expected_count) = @_; if (ref($value) eq 'ARRAY' and !$operator) { $operator = 'in'; } my $print_operator = $operator || '=>'; my $print_value; if (! defined $value) { $print_value = '(undef)'; } elsif (length($value) == 0 ) { $print_value = '""'; } elsif (ref($value) eq 'ARRAY') { $print_value = '[' . join(",", map { defined($_) ? "'$_'" : '(undef)' } @$value) . ']'; } else { $print_value = $value; } # Original non-eq-operator syntax @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) returns $expected_count item(s)"); URT::Thing->unload(); # New syntax my $property_string = "value $operator"; @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) returns $expected_count item(s)"); URT::Thing->unload(); } 63c_view_with_subviews.t.expected.cat_set.xml000444023532023421 164112121654172 23615 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t (Acme::Cat::Set owner_id => 111) Set 222 Cat 333 Cat 11b_via_to_without_type.t000444023532023421 171712121654172 17742 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; my $c1 = UR::Object::Type->define( class_name => 'Acme::Manufacturer', is => 'UR::Object', has => [ name => { is => 'Text' }, ], ); my $c2 = UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name', 'manufacturer' => { is => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius', 'manufacturer_name' => { via => 'manufacturer', to => 'name' }, ] ); my $p2 = $c2->property('manufacturer_name'); ok($p2, "got property meta for a via/to with undeclared type"); # we currently leave the data_type un-set # is($p2->data_type, "Text", "data type is set to the correct value"); is($p2->_data_type_as_class_name, "UR::Value::Text", "class for the data type is set to the correct value"); 14_ghost_objects.t000444023532023421 400512121654172 16324 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; use URT::DataSource::SomeSQLite; plan tests => 14; &setup_files_and_classes(); my $obj1 = URT::Things->get(thing_id => 1); ok($obj1, 'Loaded thing_id 1'); $obj2 = URT::Things::Ghost->get(thing_id => 2); ok (!$obj2, "Correctly couldn't load a ghost with thing_id 2"); ok($UR::Context::all_objects_loaded->{'URT::Things'}->{'1'}, 'thing_id 1 is in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things'}->{'2'}, 'thing_id 2 is not in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things::Ghost'}->{'1'}, 'thing_id 1 ghost is not in the cache'); ok(! $UR::Context::all_objects_loaded->{'URT::Things::Ghost'}->{'2'}, 'thing_id 2 ghost is not in the cache'); ok($obj1->delete(), 'thing_id 1 object deleted'); my $delobj = URT::Things->get(thing_id => 1); ok(! $delobj, 'thing_id 1 object no longer exists'); $delobj = URT::Things::Ghost->get(thing_id => 1); ok($delobj, 'thing_id 1 ghost object does exist'); 1; sub setup_files_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok( $dbh->do('create table things (thing_id integer, thing_value varchar)'), 'created table things'); ok($dbh->do("insert into things (thing_id, thing_value) values (1, 'foo')"), 'insert row 1 into things'); ok($dbh->do("insert into things (thing_id, thing_value) values (2, 'bar')"), 'insert row 2 into things'); ok($dbh->do("insert into things (thing_id, thing_value) values (3, 'foo')"), 'insert row 3 into things'); my $meta = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => [ thing_value => { is => 'String' }, ], table_name => 'THINGS', data_source => 'URT::DataSource::SomeSQLite', ); ok($meta, 'Created class for URT::Things'); } 87_attributes_have.t000444023532023421 232512121654172 16675 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; use Test::More tests => 14; class DoIt { is => 'Command', has => { i => { is_input => 1 }, o => { is_output => 1 }, p => { is_param => 1 }, } }; my $m = DoIt->__meta__; ok($m, 'got meta object for the class'); my $pi = $m->property('i'); ok($pi, 'got meta property for attribute i'); ok($pi->{is_input}, "flag is set for input"); ok(!$pi->{is_output}, "flag is not set for output"); ok(!$pi->{is_param}, "flag is not set for param"); ok($pi->is_input(), "is_input returns true"); ok(!$pi->is_output(), "is_output returns false"); ok(!$pi->is_param(), "is_output returns false"); eval { $pi->foo }; ok($@, "calling odd methods fails"); class SomeThing { has => 'x' }; my $m2 = SomeThing->__meta__; ok($m2, "got property meta for regular class"); my $px = $m2->property('x'); ok($px, 'got meta property for attribute x'); ok(!$px->{is_input}, "flag is not set for input"); eval { $px->is_input() }; ok($@, "is_input accessor attempt throws exception"); eval { $px->foo }; ok($@, "calling odd methods fails"); 40_has_many_direct.t000444023532023421 534212121654172 16624 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 20; # make sure the INDIRECT stuff still works class Order { has => [], has_many => [ lines => { is => "Line" }, line_numbers => { via => "lines", to => "line_num" }, ] }; class Line { id_by => [ order => { is => "Order", id_by => "order_id" }, line_num => { is => "Number" }, ], }; #print Data::Dumper::Dumper(Line->__meta__); my $o = Order->create( lines => [ 1, 2, 17 ] ); my @lines = $o->lines; my @line_nums = sort $o->line_numbers(); is("@line_nums", "1 17 2", "has-many with INDIRECT relationships still works correctly, now trying the new stuff..."); class FileList { has_many => [ files => { is => 'FileName' }, ] }; #print Data::Dumper::Dumper(MyCommand->__meta__); #my $m = MyCommand->__meta__->property_meta_for_name("files"); #print Data::Dumper::Dumper($m); my $list1 = FileList->create( files => ['a','b','c'] ); ok($list1, "made new object"); my @f = $list1->files(); is(scalar(@f),3,"got back expected value count"); is("@f", "a b c", "got back expected values: @f"); my $new = $list1->add_file("d"); is($new,"d","added a new value"); @f = $list1->files(); is(scalar(@f),4,"got expected value count"); is("@f","a b c d", "got expected values: '@f'"); my $list2 = FileList->create(); my $fx = $list2->file("xxx"); is($fx,undef,"correctly failed to find a made-up value"); my $f1 = $list2->add_file("aaa"); is($f1,"aaa","added a new value, retval is correct"); my $f1r = $list2->file("aaa"); is($f1r,$f1,"got it back through single accessor"); @f = $list2->files; is(scalar(@f),1,"list has expected count"); is($f[0],$f1,"items are correct"); my $f2 = $list2->add_file("bbb"); my $f2r = $list2->file("bbb"); is($f2,$f2r,"added another file and got it back correctly: $f2"); @f = $list2->files; is(scalar(@f),2,"list has expected count"); is("@f","aaa bbb","items are correct"); my (@actual,@expected); my $f3 = FileList->create(files => [qw/4 1 2 5 3/]); @expected = (4,1,2,5,3); @actual = $f3->files; is("@expected","@actual","created object has expected list"); $f3->add_file("22"); @expected = (4,1,2,5,3,22); @actual = $f3->files; is("@expected","@actual","correct after adding an item"); $f3->remove_file("5"); @expected = (4,1,2,3,22); @actual = $f3->files; is("@expected","@actual","correct after removing an item"); $a = [qw/11 22 33/]; $f3->files($a); @expected = (11,22,33); @actual = $f3->files; is("@expected","@actual","correct after setting an item"); push @$a,"44"; is("@expected","@actual","changing the arrayref after setting it has no effect, as expected"); 04e_file_track_open_close.t000444023532023421 2571412121654172 20176 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 100; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace use File::Temp; # The file tracking stuff is defined at the bottom of this file my ($file_new, $file_open, $file_close, $file_DESTROY, $file_seek, $file_seek_pos, $file_tell); IO::File::Tracker->config_callbacks( 'new' => sub { no warnings 'uninitialized'; $file_new++ }, 'open' => sub { no warnings 'uninitialized'; $file_open++ }, 'close' => sub { no warnings 'uninitialized'; $file_close++ }, 'DESTROY' => sub { no warnings 'uninitialized'; $file_DESTROY++ }, 'seek' => sub { no warnings 'uninitialized'; $file_seek_pos = $_[0]; $file_seek++ }, 'tell' => sub { no warnings 'uninitialized'; $file_tell++ }, ); sub clear_trackers { $file_new = 0; $file_open = 0; $file_close = 0; $file_DESTROY = 0; $file_seek = 0; $file_seek_pos = undef; $file_tell = 0; }; my $file_line_length = 8; # Includes the newline my $file_data = qq(1\tAAA\t1 2\tBBB\t1 3\tCCC\t1 4\tDDD\t1 5\tEEE\t1 6\tfff\t0 7\tggg\t0 8\thhh\t0 9\tiii\t0 ); # First, make up a File datasource with the default behavior of keeping its file # handle open as long as possible my(undef,$tempfile_name_1) = File::Temp::tempfile(); END { unlink $tempfile_name_1 } my $fh_1 = IO::File->new($tempfile_name_1, 'w'); $fh_1->print($file_data); $fh_1->close(); my $keepopen_ds = UR::DataSource::File->create( delimiter => "\t", quick_disconnect => 0, handle_class => 'IO::File::Tracker', server => $tempfile_name_1, column_order => ['letter_id', 'name', 'is_upper'], sort_order => ['letter_id'], ); UR::Object::Type->define( class_name => 'URT::Letters', id_by => 'letter_id', has => [ letter_id => { is => 'Integer' }, name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $keepopen_ds->id, ); &clear_trackers(); my $obj = URT::Letters->get(1); ok($obj, 'Got an object from the file'); is($obj->name, 'AAA', 'it has the correct name'); ok($file_new, 'new() was called on the file handle'); ok($file_open, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = URT::Letters->get(2); ok($obj, 'Got second object from the file'); is($obj->name, 'BBB', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = URT::Letters->get(5); ok($obj, 'Got fifth object from the file'); is($obj->name, 'EEE', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() was to the correct position'); # This one should still be in the data source's cache &clear_trackers(); $obj = URT::Letters->get(4); ok($obj, 'Got fourth object'); is($obj->name, 'DDD', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() was to the correct position'); # This datasource points to the same file (not a problem since we're not writing to it) # but with the quick_disconnect flag on my $close_ds = UR::DataSource::File->create( delimiter => "\t", quick_disconnect => 1, handle_class => 'IO::File::Tracker', server => $tempfile_name_1, column_order => ['letter_id', 'name', 'is_upper'], sort_order => ['letter_id'], ); UR::Object::Type->define( class_name => 'URT::LettersAlternate', id_by => 'letter_id', has => [ letter_id => { is => 'Integer' }, name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $close_ds->id, ); # Create a couple of iterators on the same datasource and interleave their # reads, and make sure they seek back to the correct positions &clear_trackers(); my $lower_iter = URT::LettersAlternate->create_iterator(is_upper => 0); ok($lower_iter, 'Created an iterator for lower case objects'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got an object from the lower case iterator'); is($obj->name, 'fff', 'It was the first lowercase object'); is($file_new, 1, 'new() was called on the file handle'); is($file_open, 1, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() was to the correct position'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got another object from the lower case iterator'); is($obj->name, 'ggg', 'It was the next lowercase object'); is($file_new, 0, 'new() was called on the file handle'); is($file_open, 0, 'open() was called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); # This get() won't close the handle because $all_iter is still running $obj = URT::LettersAlternate->get(9); ok($obj, 'Use get() to get the ninth object'); is($obj->name, 'iii', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 7, 'seek() set the file pos to the 7th line'); # Because the lower-case iter gets us this far &clear_trackers(); my $upper_iter = URT::LettersAlternate->create_iterator(is_upper => 1); ok($upper_iter, 'Created an iterator for upper case objects'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'AAA', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, 0, 'seek() set the file pos to 0'); &clear_trackers(); $obj = $lower_iter->next(); ok($obj, 'Got an object from the lower case iterator'); is($obj->name, 'hhh', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 8, 'seek() set the file pos to the 8th line'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'BBB', 'The name was correct'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length * 2, 'seek() set the file pos to the 1th (second) line'); &clear_trackers(); $lower_iter = undef; #diag('Closing the lower case object iterator'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $obj = $upper_iter->next(); ok($obj, 'Got an object from the upper case iterator'); is($obj->name, 'CCC', 'It was the third object'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 0, 'close() was not called on the file handle'); is($file_seek, 0, 'seek() was not called on the file handle'); &clear_trackers(); $upper_iter = undef; #diag('Closing the upper case object iterator'); is($file_new, 0, 'new() was not called on the file handle'); is($file_open, 0, 'open() was not called on the file handle'); is($file_close, 1, 'close() was called on the file handle'); is($file_seek, 0, 'seek() was called on the file handle'); &clear_trackers(); $obj = URT::LettersAlternate->get(5); # something not in the object cache so it will hit the data source ok($obj, 'Got object with id 5'); is($obj->name, 'EEE', 'It has the right name'); is($file_new, 1, 'new() was called on the file handle'); is($file_open, 1, 'open() was called on the file handle'); is($file_close, 1, 'close() was called on the file handle'); is($file_seek, 1, 'seek() was called on the file handle'); is($file_seek_pos, $file_line_length*3, 'seek() was to the correct position'); # The uppercase iter gets us this far sub IO::File::Tracker::config_callbacks { my $class = shift; my %set_callbacks = @_; foreach my $key ( keys %set_callbacks) { $IO::File::Tracker::callbacks{$key} = $set_callbacks{$key}; } } sub IO::File::Tracker::_call_cb { my($op, @args) = @_; my $cb = $IO::File::Tracker::callbacks{$op}; if ($cb) { $cb->(@args); } } BEGIN { @IO::File::Tracker::ISA = qw( IO::File ); # Create overridden methods for the ones we want to track foreach my $subname (qw( new open close DESTROY seek tell getline ) ) { no strict 'refs'; my $subref = sub { my $self = shift; IO::File::Tracker::_call_cb($subname, @_); my $super = IO::File->can($subname); return $super->($self, @_); }; my $fq_subname = 'IO::File::Tracker::'.$subname; *$fq_subname = $subref; } } 24_query_by_is_calculated.t000444023532023421 301112121654172 20177 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_type varchar)'), 'created product table'); ok($dbh->do("insert into product values (1,'race car', 'cool')"), 'insert row into product for race car'); ok($dbh->do("insert into product values (2,'pencil','notcool')"), 'insert row into product for pencil'); UR::Object::Type->define( class_name => 'URT::Product', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_type => { is => 'Text' }, is_cool => { is => 'Boolean', calculate_from => 'product_type', calculate => q( return ($product_type eq 'cool') ? 1 : 0 ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); my @p = URT::Product->get(is_cool => 1); is(scalar(@p), 1, 'Got one product that is_cool'); is($p[0]->product_name, 'race car', 'name is correct'); @p = URT::Product->get(is_cool => 0); is(scalar(@p), 1, 'Got one product that is not is_cool'); is($p[0]->product_name, 'pencil', 'name is correct'); @p = URT::Product->get(-hints => ['is_cool']); is(scalar(@p), 2, 'Getting products with -hints => is_cool got 2 items'); 51b_unmatched_hints_query_cache.t000444023532023421 1115012121654172 21376 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT; use Test::More tests => 23; # When doing a get that includes a delegated property, and the delegation # does not match anything, make sure a later query correctly does not re-query # the database my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); $dbh->do('create table manufacturer (mfg_id integer NOT NULL PRIMARY KEY, name varchar)'); $dbh->do('create table model (model_id integer NOT NULL PRIMARY KEY, name varchar, mfg_id integer REFERENCES manufacturer(mfg_id))'); my $insert = $dbh->prepare('insert into manufacturer values (?,?)'); ok($insert, 'Insert manufacturers'); foreach my $row ( [1,'Ford'], [2,'Toyota'], [3,'Packard']) { $insert->execute(@$row); } $insert->finish; # Ford has 2 models: Focus and F150 # Toyota has 2 models: Prius and Tundra # Packard and Desoto have no models $insert = $dbh->prepare('insert into model values (?,?,?)'); ok($insert, 'Insert models'); foreach my $row ( [1,'Focus',1], [2,'F150',1], [3,'Prius',2], [4,'Tundra', 2] ) { $insert->execute(@$row); } $insert->finish; UR::Object::Type->define( class_name => 'URT::Manufacturer', data_source => 'URT::DataSource::SomeSQLite', table_name => 'manufacturer', id_by => [ mfg_id => { is => 'integer' }, ], has => [ name => { is => 'String' }, models => { is => 'URT::Model', is_many => 1, reverse_as => 'manufacturer', is_optional => 1 }, #model_ids => { via => 'models', to => 'model_id', is_many => 1, is_optional => 1 }, model_ids => { via => 'models', to => 'model_id', is_many => 1, is_optional => 1 }, ], ); UR::Object::Type->define( class_name => 'URT::Model', table_name => 'model', data_source => 'URT::DataSource::SomeSQLite', id_by => [ model_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, manufacturer => { is => 'URT::Manufacturer', id_by => 'mfg_id' }, manufacturer_name => { via => 'manufacturer', to => 'name' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); # Test a get() with hints $query_count = 0; my @mfg = URT::Manufacturer->get(id => 1, -hints => ['model_ids']); is(scalar(@mfg),1, 'Got 1 manufacturer with id 1'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @models = URT::Model->get(1); # model_id 1 should have been loaded by the above mfg get() is(scalar(@models), 1, 'Get model by id 1 got one object'); is($query_count, 0, 'Made no queries'); $query_count = 0; @models = URT::Model->get(mfg_id => 1); # These should also have been loaded before is(scalar(@models), 2, 'Two models with mfg_id => 1'); is($query_count, 0, 'Made no queries'); # Test a get() with a delegated property $query_count = 0; @mfg = URT::Manufacturer->get(model_ids => 3); is(scalar(@mfg), 1, 'Got 1 manufacturer with model_id 3'); is($mfg[0]->name, 'Toyota', 'Was the right manufacturer'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(model_id => 3); # Should have been loaded by mfg get() with model_id 3 is(scalar(@models), 1, 'Got 1 model with model_id 3'); is($query_count, 0, 'Made no queries'); # test a get() with hints where the hinted property matches nothing $query_count = 0; @mfg = URT::Manufacturer->get(id => 3, -hints => ['model_ids']); is(scalar(@mfg), 1, 'Got 1 manufacturer with id 3'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(mfg_id => 3); # Should have been loaded as part of the mfg get() with id 3 is(scalar(@models), 0, 'Got no models with mfg_id 3'); is($query_count, 0, 'Made no queries'); # This is to avoid an additional query in the next get() when objects are # indexed. It's a side-effect of model_ids being is_many, and the Index # not indexing by is_many properties URT::Model->get(mfg_id => 2); # Test a get() by delegated property that matches nothing $query_count = 0; @mfg = URT::Manufacturer->get(model_ids => 99); is(scalar(@mfg), 0, 'Got no manufacturers with model_id 99'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @models = URT::Model->get(model_id => 99); is(scalar(@models), 0, 'Got no models with model_id 99'); SKIP: { skip "via properties don't record info in all_params_loaded yet", 1; is($query_count, 0, 'Made no queries'); } 1; 39_has_many.t000444023532023421 1132712121654172 15322 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 23; class Animal { has => [ fur => { is => 'Text' }, # Two an indirect properties # referencing a single value # via another object # through a has-many # ..and they're writable. # one is to a regular property limbs => { is => 'Animal::Limb', reverse_as => 'animal', is_mutable => 1, is_many => 1 }, foreleg_flexibility_score => { via => 'limbs', where => [ number => 1 ], to => 'flexibility_score', is_mutable => 1, }, # one is "to" an id property, notes => { is => 'Animal::Note', reverse_as => 'animal', is_mutable => 1, is_many => 1 }, primary_note_text => { via => 'notes', where => [ type => 'primary' ], to => 'text', is_mutable => 1 }, eyes => { is => 'Animal::Eye', reverse_as => 'animal', is_many => 1 }, antlers => { is => 'Animal::Antler', reverse_as => 'animal', is_many => 1 }, ], }; class Animal::Limb { id_by => [ animal => { is => 'Animal', id_by => 'animal_id' }, number => { is => 'Number' }, ], has => [ flexibility_score => { is => 'Number', is_optional => 1 }, ] }; class Animal::Note { id_by => [ animal => { is => 'Animal', id_by => 'animal_id' }, type => { is => 'Text' }, text => { is => 'Text' }, ] }; class Animal::Eye { has => [ animal => { is => 'Animal', id_by => 'animal_id' }, color => { is => 'String' }, ], }; class Animal::Antler { has => [ animal => { is => 'Animal', id_by => 'animal_id' }, pointiness => { is => 'Number' }, ], }; # make an example object my $a = Animal->create(); ok($a, 'new animal'); # add parts the hard way my $i1 = $a->add_limb(number => 1); ok ($i1, 'has one foot.'); my $i2 = $a->add_limb(number => 2); ok ($i2, 'has two feet!'); # make another, and add them in a slightly easier way my $a2 = Animal->create( limbs => [ { number => 1, flexibility_score => 11 }, { number => 2, flexibility_score => 22 }, { number => 3, flexibility_score => 33 }, { number => 4, flexibility_score => 44 }, ], fur => "fluffy", ); ok($a2, 'yet another animal'); my @i = $a2->limbs(); is(scalar(@i),4, 'expected 4 feet!'); # make a third object, and add them the easiest way my $a3 = Animal->create( limbs => [1,2,3,4], fur => "fluffy", ); ok($a3, 'more animals'); my @i2= $a3->limbs(); is(scalar(@i2),4, '4 feet again, the easy way'); # indirect access.. my $note1 = $a3->add_note(type => 'primary', text => "note1"); ok($note1, "made a note"); my $note2 = $a3->add_note(type => 'secondary', text => "note2"); ok($note2, "made another note"); my $t = $a3->primary_note_text("note1b"); is($t,"note1b", "set a remote partial-id-value through the indirect accessor"); $t = $a3->primary_note_text(); is($t,"note1b","got back the partial-id-value through the indirect accessor"); my $s = $a3->foreleg_flexibility_score(100); is($s,100,"set a remote non-id value through the indirect accessor"); $s = $a3->foreleg_flexibility_score(); is($s,100,"got back the non-id value through the indirect accessor"); # Give animal 3 two eyes of different colors # We're avoiding the add_eye method so the rule/template captured by the # method's closure isn't pre-created when we use the filterable accessor Animal::Eye->create(animal => $a3, color => 'blue'); Animal::Eye->create(animal => $a3, color => 'green'); my $eye = $a3->eye(color => 'green'); ok($eye, 'Got an eye via the filterable accessor'); is($eye->color, 'green', 'It is the correct eye'); $eye = $a3->eye(color => 'blue'); ok($eye, 'Got an eye via the filterable accessor'); is($eye->color, 'blue', 'It is the correct eye'); $eye = $a3->eye(color => 'tractor'); ok(! $eye, 'Correctly found no eye via the filterable accessor'); # Do it again with the missing thing first # and use the plural accessor to test that one out too Animal::Antler->create(animal => $a3, pointiness => 1); Animal::Antler->create(animal => $a3, pointiness => 2); my $antler = $a3->antlers(pointiness => 100); ok(! $antler, 'Correctly found no antler via the filterable accessor'); $antler = $a3->antlers(pointiness => 1); ok($antler, 'Got an antler via the filterable accessor'); is($antler->pointiness, 1, 'It is the correct antler'); $antler = $a3->antlers(pointiness => 2); ok($antler, 'Got an antler via the filterable accessor'); is($antler->pointiness, 2, 'It is the correct antler'); 58_order_by_merge_changed_objects.t000444023532023421 674412121654172 21661 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 19; # There are 2 things in the DB and one newly created thing that satisfy # the get() request. But one of the DB items has been changed in the # object cache, and sorts in a different order than the order returned by # the DB query &setup_classes_and_db(); # Change something in memory and see if it'll be honored in the results # It now sorts last in the DB, but first in the object cache my $o = URT::Thing->get(2); $o->data('aaaa'); # Create a new thing my $new_obj= URT::Thing->create(name => 'Bobert', data => 'abc'); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); is(scalar(@o), 3, 'Got 3 things with name like Bob%'); is($o[0]->id, 2, 'thing_id == 2 is first in the list'); # The changed thing is($o[0]->name, 'Bob', 'its name is Bob'); is($o[0]->data, 'aaaa', 'its data is foo'); is($o[1], $new_obj, 'Second item in the list is the newly created Thing'); is($o[2]->id, 4, 'thing_id == 4 is third in the list'); is($o[2]->name, 'Bobby', 'its name is Bobby'); is($o[2]->data, 'baz', 'its data is baz'); # This originally sorted first. Change it so it sorts last $o = URT::Thing->get(1); $o->data('zzz'); $new_obj = URT::Thing->create(name => 'Joeseph', data => 'mmm'); # Should find Joey (data => ccc), Joeseph (data mmm) and Joe (data zzz, originally aaa) @o = URT::Thing->get('name like' => 'Joe%', -order => ['data']); is(scalar(@o), 3, 'Got three things with name like Joe%'); is($o[0]->id, 5, 'thing_id == 5 is first in the list'); is($o[0]->name, 'Joey', 'its name is Joey'); is($o[0]->data, 'ccc', 'its data is ccc'); is($o[1], $new_obj, 'Second item in the list is the newly created Thing'); is($o[2]->id, 1, 'thing_id == 1 is third in the list'); # The changed thing is($o[2]->name, 'Joe', 'its name is Joe'); is($o[2]->data, 'zzz', 'its data is zzz'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [1, 'Joe', 'aaa'], [2, 'Bob', 'zzz'], [3, 'Fred', 'quux'], [4, 'Bobby', 'baz'], [5, 'Joey', 'ccc'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 03b_rule_constant_values.t000444023532023421 142412121654172 20070 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests=> 2; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; class URT::Foo { has => [qw/a b c/]}; my @p1a = (-order_by => [qw/b/], -group_by => [qw/b a/]); my $bx1 = URT::Foo->define_boolexpr(@p1a); my @p1b = $bx1->params_list; is(Data::Dumper::Dumper(\@p1a),Data::Dumper::Dumper(\@p1b), "params list is symmetrical for an expression with two constant values"); my $bx2 = $bx1->normalize; my @p2a = (-group_by => [qw/b a/], -order_by => [qw/b/]); my @p2b = $bx2->params_list; is(Data::Dumper::Dumper(\@p2a),Data::Dumper::Dumper(\@p2b), "params list is symmetrical for an expression with two constant values after normalize"); 73_opts_spec_creation_and_validation.t000444023532023421 405012121654172 22413 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More; use File::Temp; BEGIN { eval "use Getopt::Complete::Cache;"; if ($@ =~ qr(Can't locate Getopt/Complete/Cache.pm in \@INC)) { plan skip_all => 'Getopt::Complete::Cache does not exist on the system'; } else { plan tests => 11; # This should match the number of keys in %tests below use_ok('Getopt::Complete::Cache'); } } my $fh = File::Temp->new(); my $fname = $fh->filename; # Create the file my $cmd = UR::Namespace::Command::Update::TabCompletionSpec->create(classname => 'UR::Namespace::Command', output => $fname); ok($cmd, 'Created command object'); $cmd->dump_error_messages(0); $cmd->dump_warning_messages(0); $cmd->queue_error_messages(1); $cmd->queue_warning_messages(1); ok($cmd->execute(), 'creating ur spec file in tmp'); my @messages = $cmd->warning_messages(); ok(!scalar(@messages), 'executing command generated no warning messages'); @messages = $cmd->error_messages(); is(scalar(@messages), 1, 'executing command generated one error message'); like($messages[0], qr/Command\.pm\.opts is 0 bytes, reverting to previous/, 'Error message was correct'); # Try loading/parsing the file ok(-f $fname, 'Output options file exists'); my $content = join('', $fh->getlines); my $spec = eval $content; is($@, '', 'eval of spec file worked'); # first look for >define, the next item in the list is subcommands for define my $found = 0; for (my $i = 0; $i < @$spec; $i++) { if ($spec->[$i] eq '>define') { $found = 1; $spec = $spec->[$i+1]; last; } } ok($found, 'Found define top-level command data'); $found = 0; for (my $i = 0; $i < @$spec; $i++) { if ($spec->[$i] eq '>namespace') { $found = 1; last; } } ok($found, 'Found define namespace command data'); # Try importing the file my $rv = Getopt::Complete::Cache->import(file => $fname, above => 1, comp_cword => 1); is($rv, 1, 'importing ur spec from tmp'); 03j_or_rules_with_meta.t000444023532023421 1063512121654172 17560 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 32; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, parant_name => { is => 'String', via => 'parent', to => 'name' }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, nicknames => { is_many => 1, is => "Integer" }, ], }; # First an easy one.... my $bx = URT::FancyItem->define_boolexpr(name => 'Fred', -order => [ 'bar' ]); ok($bx, 'Made a simple rule with -order'); ok($bx->specifies_value_for('name'), 'Rule has value for name'); is($bx->value_for('name'), 'Fred', 'Rule has correct value for for name'); ok(! $bx->specifies_value_for('foo'), 'Rule correctly has no value for foo'); is_deeply($bx->value_for('-order'), ['bar'], 'Rule has correct value for -order'); # Try a compound rule $bx = URT::FancyItem->define_boolexpr(-or => [ [ name => 'Fred' ], [foo => 'bar'] ], -order => [ 'bar' ]); ok($bx, 'Make Or-type rule with -order'); my @underlying = $bx->underlying_rules(); is(scalar(@underlying), 2, 'There were 2 underlying rules'); ok($underlying[0]->specifies_value_for('name'), 'First underlying rule has value for name'); is($underlying[0]->value_for('name'), 'Fred', 'First underlying rule has correct value for for name'); ok(! $underlying[0]->specifies_value_for('foo'), 'First underlying rule correctly has no value for foo'); is_deeply($underlying[0]->value_for('-order'), ['bar'], 'First underlying rule has correct value for -order'); ok(! $underlying[1]->specifies_value_for('name'), 'Second underlying rule correctly has no value for name'); ok($underlying[1]->specifies_value_for('foo'), 'Second underlying rule has value for foo'); is($underlying[1]->value_for('foo'), 'bar', 'Second underlying rule has correct value for for name'); is_deeply($underlying[1]->value_for('-order'), ['bar'], 'Second underlying rule has correct value for -order'); # another compound rule with 3 parts $bx = URT::FancyItem->define_boolexpr(-or => [ [ name => 'Fred' ], [foo => 'bar'], ['score >' => 3 ]], -hints => ['bar','parent_name']); ok($bx, 'Make Or-type rule with -hints'); @underlying = $bx->underlying_rules(); is(scalar(@underlying), 3, 'There were 3 underlying rules'); ok($underlying[0]->specifies_value_for('name'), 'First underlying rule has value for name'); is($underlying[0]->value_for('name'), 'Fred', 'First underlying rule has correct value for for name'); ok(! $underlying[0]->specifies_value_for('foo'), 'First underlying rule correctly has no value for foo'); ok(! $underlying[0]->specifies_value_for('score'), 'First underlying rule correctly has no value for score'); is_deeply($underlying[0]->value_for('-hints'), ['bar','parent_name'], 'First underlying rule has correct value for -hints'); ok(! $underlying[1]->specifies_value_for('name'), 'Second underlying rule correctly has no value for name'); ok($underlying[1]->specifies_value_for('foo'), 'Second underlying rule has value for foo'); is($underlying[1]->value_for('foo'), 'bar', 'Second underlying rule has correct value for for name'); ok(! $underlying[1]->specifies_value_for('score'), 'Second underlying rule correctly has no value for score'); is_deeply($underlying[1]->value_for('-hints'), ['bar','parent_name'], 'Second underlying rule has correct value for -hints'); ok(! $underlying[2]->specifies_value_for('name'), 'Third underlying rule has value for name'); ok(! $underlying[2]->specifies_value_for('foo'), 'Third underlying rule correctly has no value for foo'); ok($underlying[2]->specifies_value_for('score'), 'Third underlying rule has value for score'); is($underlying[2]->value_for('score'), 3, 'Third underlying rule has correct value for for score'); is_deeply($underlying[2]->value_for('-hints'), ['bar','parent_name'], 'Third underlying rule has correct value for -hints'); 50b_get_via_sql.t000444023532023421 1034612121654172 16153 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok( $dbh->do('create table related_thing (thing_id integer not null primary key, name varchar not null)'), 'create related_thing table'); ok( $dbh->do('create table thing (thing_id integer not null primary key, name varchar not null, related_id integer REFERENCES related_thing(thing_id))'), 'create thing table'); my $insert_related = $dbh->prepare('insert into related_thing values (?,?)'); ok($insert_related, 'prepare to insert to related_thing'); $insert_related->execute(11,'red'); $insert_related->execute(12,'blue'); $insert_related->execute(13,'green'); $insert_related->finish(); my $insert_thing = $dbh->prepare('insert into thing values (?,?,?)'); ok($insert_thing, 'prepare to insert to thing'); $insert_thing->execute(1,'pink',11); $insert_thing->execute(2,'cornflower',12); $insert_thing->execute(3,'turquoise',13); $insert_thing->finish(); ok($dbh->commit,'Commit data to DB'); UR::Object::Type->define( class_name => 'URT::RelatedThing', id_by => 'thing_id', has => [ 'name' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related_thing', ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', # not the same as related_thing.thing_id has => [ name => { is => 'String' }, related => { is => 'URT::RelatedThing', id_by => 'related_id' } ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # Do a full join so the IDs returned by the SQL are duplicated my @things = URT::Thing->get(sql => 'select thing.thing_id from thing join related_thing'); is(scalar(@things), 3, 'Got 3 things'); is_deeply([map { $_->id } @things], [1,2,3], 'IDs are correct'); @things = URT::Thing->get(sql => 'select * from thing order by thing_id DESC'); is(scalar(@things), 3, 'Got 3 things'); is_deeply([map { $_->id } @things], [1,2,3], 'IDs are correct'); @things = eval { URT::Thing->get(sql => 'select name from thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: thing_id}s, 'got exception from SQL without primary key'); is(scalar(@things), 0, 'Returned 0 things'); @things = URT::Thing->get(sql => ['select thing_id from thing where name = ?', 'pink']); is(scalar(@things), 1, 'Got 1 thing with name pink using SQL with a placeholder'); is($things[0]->id, 1, 'It was the right ID'); @things = eval { URT::Thing->get(sql => ['select thing_id from thing where name = ? and thing_id = ?', 'pink']) }; like($@, qr{The number of params supplied \(1\) does not match the number of placeholders \(2\)}, 'got exception from SQL without primary key'); is(scalar(@things), 0, 'Returned 0 things'); ok( $dbh->do('create table multi_thing (id1 integer not null, id2 integer not null, name varchar, primary key(id1,id2))'), 'Create table with 2 primary keys'); my $multi_insert = $dbh->prepare('insert into multi_thing values (?,?,?)'); $multi_insert->execute(1,1,'bob'); $multi_insert->execute(1,2,'bob'); $multi_insert->execute(2,1,'fred'); $multi_insert->execute(2,2,'fred'); UR::Object::Type->define( class_name => 'URT::MultiThing', id_by => ['id1', 'id2'], has => ['name'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'multi_thing', ); @things = URT::MultiThing->get(sql => 'select * from multi_thing order by id2'); is(scalar(@things), 4, 'Got 4 items from multi_thing table'); is_deeply([map { $_->id } @things], ["1\t1","1\t2","2\t1","2\t2"], 'Objects returned in the right order'); @things = eval { URT::MultiThing->get(sql => 'select id1 from multi_thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: id2}s, 'got exception from SQL missing one primary key'); @things = eval { URT::MultiThing->get(sql => 'select name from multi_thing') }; like($@, qr{The SQL supplied is missing one or more ID columns.*?missing: id1, id2}s, 'got exception from SQL missing both primary keys'); 43_infer_values_from_rule.t000444023532023421 760612121654172 20237 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More; plan tests => 27; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; UR::DBI->no_commit(1); &create_test_data(); my($rule,$value,@values); my $context = UR::Context->get_current; $rule = UR::BoolExpr->resolve('URT::43Primary', primary_id => 1, primary_value => 'One'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'get a value directly in the rule'); $rule = UR::BoolExpr->resolve('URT::43Primary', rel_id => 1); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'infer a direct property with a rule also containing a different direct property'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, 1, 'infer an indirect property with a rule containing a direct property'); $rule = UR::BoolExpr->resolve('URT::43Primary', related_value => '1'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('rel_id', $rule); is($value, 1, 'infer a direct linking property with a rule containing an indirect property'); $value = $context->infer_property_value_from_rule('primary_id', $rule); is($value, 1, 'infer a direct property with a rule containing an indirect property'); $rule = UR::BoolExpr->resolve('URT::43Primary', related_value => '2'); ok($rule, 'Create rule'); @values = $context->infer_property_value_from_rule('primary_id', $rule); @values = sort @values; is(scalar(@values), 2, 'inferring a direct property with a rule containing an indirect property matching 2 objects'); is($values[0], 2, 'matched first primary_id'); is($values[1], 3, 'matched second primary_id'); # This ends up returning '3' because there's a Related object with related_id => 3 # though there is no Primary object with a rel_id => 3 #$rule = UR::BoolExpr->resolve('URT::43Primary', rel_id => 3); #ok($rule, 'Create rule'); #$value = $context->infer_property_value_from_rule('related_value', $rule); #is($value, undef, 'infer an indirect property with a rule containing a direct property matching nothing correctly returns undef'); $rule = UR::BoolExpr->resolve('URT::43Related', related_id => 2); ok($rule, 'Create rule'); @values = $context->infer_property_value_from_rule('primary_values', $rule); @values = sort {$a cmp $b} @values; is(scalar(@values), 2, 'infer an indirect, reverse_as property with a rule containing a direct property'); is($values[0], 'Three', 'first inferred value was correct'); is($values[1], 'Two', 'first inferred value was correct'); $rule = UR::BoolExpr->resolve('URT::43Related', primary_values => 'One'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, '1', 'infer direct property with a rule containing an indirect, reverse_as property'); $rule = UR::BoolExpr->resolve('URT::43Related', primary_values => 'Two'); ok($rule, 'Create rule'); $value = $context->infer_property_value_from_rule('related_value', $rule); is($value, '2', 'infer direct property with a rule containing an indirect, reverse_as property'); sub create_test_data { ok(URT::43Primary->create(primary_id => 1, primary_value => 'One',rel_id => 1), 'Create test object'); ok(URT::43Primary->create(primary_id => 2, primary_value => 'Two',rel_id => 2), 'Create test object'); ok(URT::43Primary->create(primary_id => 3, primary_value => 'Three',rel_id => 2), 'Create test object'); ok(URT::43Primary->create(primary_id => 4, primary_value => 'Four',rel_id => 4), 'Create test object'); ok(URT::43Related->create(related_id => 1, related_value => '1'), 'Create test object'); ok(URT::43Related->create(related_id => 2, related_value => '2'), 'Create test object'); ok(URT::43Related->create(related_id => 3, related_value => '3'), 'Create test object'); } 99_transaction.t000444023532023421 2756512121654172 16071 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; #BEGIN { $ENV{UR_CONTEXT_BASE} = "URT::Context::Testing" }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use DBI; use IO::Pipe; use Test::More; use UR::Value::SloppyPrimitive; use UR::Value::SCALAR; our @test_input = ( ["URT::Foo" => "f1","f2"], ["URT::Bar" => "b1","b2"], ); our $num_classes = scalar(@test_input); our $num_trans = 5; if ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'slow and not needed at install, just at dev time'; } else { plan tests => ((($num_trans * 6) * $num_classes) + 1); } use Data::Dumper; use Data::Compare; # With Purity on (which UR::Util::deep_copy does), Data::Dumper::Dumper complains when it # encounters code refs with no way to disable the warning message. This is an underhanded # way of disabling it. use Carp; $Data::Dumper::Useperl = 1; { no warnings 'redefine'; *Data::Dumper::carp = sub { 1; }; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use UR::Change; use UR::Context; use UR::Context::Transaction; use UR::DataSource; sub dump_states { my ($before,$after); use YAML; #$DB::single = 1; IO::File->new(">before.yml")->print(YAML::Dump($before)); IO::File->new(">after.yml")->print(YAML::Dump($after)); } note("this is a slow test because it copies does deep diffs of large data trees at each step"); ########################################### sub take_state_snapshot { my $state = {}; my $cx = $UR::Context::current; my @classes = sort UR::Object->subclasses_loaded; for my $class_name (@classes) { next if $class_name->isa("UR::Singleton"); my @objects = sort { $a->id cmp $b->id } $cx->all_objects_loaded_unsubclassed($class_name); next unless @objects; next if $class_name eq "UR::Object::Index"; next if $class_name eq "UR::Namespace::CommandParam"; next if $class_name =~ /UR::BoolExpr.*/; next if $class_name eq 'UR::Context::Transaction'; next if $class_name eq 'UR::Change'; next if $class_name->isa("UR::Value"); for my $object (@objects) { next if $class_name->isa("UR::Object::Type") and $object->class_name->isa("UR::Value"); next if $class_name->isa("UR::Value::Type"); $state->{$class_name} ||= {}; my $copy = UR::Util::deep_copy($object); delete $copy->{_change_count}; delete $copy->{_request_count}; delete $copy->{__get_serial}; if ($class_name->isa('UR::Object::Type')) { delete $copy->{get_composite_id_decomposer}; delete $copy->{_ordered_inherited_class_names}; delete $copy->{_all_property_type_names}; delete $copy->{'_unique_property_sets'}; delete $copy->{_all_property_names}; delete $copy->{_all_id_property_names}; delete $copy->{_id_property_sorter}; delete $copy->{_id_property_names}; delete $copy->{_sorter}; delete $copy->{_property_meta_for_name}; delete $copy->{db_committed}{_id_property_sorter}; delete $copy->{db_committed}{_property_meta_for_name}; delete $copy->{db_committed}{_sorter}; delete $copy->{get_composite_id_resolver}; delete $copy->{_property_name_class_map}; delete $copy->{_resolve_property_aliases}; delete $copy->{cache}; } if ($class_name->isa('UR::Object::Property')) { delete $copy->{_is_numeric}; delete $copy->{_data_type_as_class_name}; delete $copy->{_get_property_name_pairs_for_join}; } for my $key (keys %$copy) { if (! defined $copy->{$key}) { delete $copy->{$key}; } elsif (ref($copy->{$key}) eq "ARRAY") { for my $value (@{ $copy->{$key} }) { $value = "CODE REPLACEMENT" if ref($value) eq "CODE"; } } elsif (ref($copy->{$key}) eq "HASH") { for my $key (keys %{ $copy->{$key} }) { $copy->{$key} = "CODE REPLACEMENT" if ref($copy->{$key}) eq "CODE"; } } elsif (ref($copy->{$key}) eq "CODE") { $copy->{$key} = "CODE REPLACEMENT"; } } $state->{$class_name}{$object->id} = $copy; } } return $state; } # These represent the state of the test, and are managed by the subs below. my ($o0, $o1, $o2, $o3, $o4, $o5, $o6, $o7, $o8); my ($state_initial, $state_final); my @transactions; my @transaction_prior_states; my $test_obj_id; sub clear { # wipe everything, reset the id for test objects UR::Context->rollback(); UR::Context->clear_cache(); ($o0, $o1, $o2, $o3, $o4, $o5, $o6, $o7, $o8) = (); ($state_initial, $state_final) = (); @transactions = (); @transaction_prior_states = (); $test_obj_id = 100; } sub init { my ($class_to_test, $property1, $property2) = @_; # pre-transactions: take a snapshot $state_initial = take_state_snapshot(); # make some changes before starting any transactions # these should never be reversed $o0 = $class_to_test->create(id => $test_obj_id, $property1 => 'value0'); ## t0 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o1 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value1"); $o2 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value2"); $o3 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value3"); ## t1 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o2->delete; $o3->$property1("value3changed"); $o4 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value4"); ## t2 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); # change an old unchanged $o4->$property1("value4changed"); # change a different part of a changed object $o3->$property2("value3${property2}changed"); #UR::Context->_sync_databases(); # change a new object $o5 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value5"); $o5->$property1("value5changed"); # change something twice $o6 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value6"); $o6->$property2("value6changed1"); $o6->$property2("value6changed2"); # make something new and then delete it in the same transactions $o7 = $class_to_test->create(id => ++$test_obj_id, $property1 => "value7"); $o7->delete; ## t3 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); # re-create deleted object $o8 = $class_to_test->create(id => $test_obj_id, $property1 => "value8recreated7"); # delete changed object $o6->delete(); ## t4 push @transaction_prior_states, take_state_snapshot(); push @transactions, UR::Context::Transaction->begin(); $o8->delete(); # post-transactions: get a final snapshot $state_final = take_state_snapshot(); } sub rollback_and_verify { my $n = shift; my $msg = shift; my $t = $transactions[$n]; ok($t->rollback, "rolled back transactions $n " . $msg); my $state_now = take_state_snapshot(); my $state_then = $transaction_prior_states[$n]; is_deeply($state_now, $state_then, "application state now matches pre-transaction state for $n " . $msg) or diag(compare_snapshots($state_then,$state_now)); #$DB::single = 1; print ""; } ########################################### # find or create each class we'll test for my $spec (@test_input) { my ($class_name, @property_names) = @$spec; if (UR::Object::Type->get($class_name)) { next; } UR::Object::Type->define( class_name => $class_name, has => \@property_names ); # this dynamically loads, but messes up diffs because of it. #$class_name->generate_support_class("Ghost"); } # ensure that the logic in clear() really takes us back to the starting point my $state_at_test_start = take_state_snapshot(); #$DB::single = 1; clear(); my $state_after_initial_clear = take_state_snapshot(); is_deeply($state_after_initial_clear, $state_at_test_start, "clear returns restores state with no changes"); #dump_states($state_at_test_start,$state_after_initial_clear); # test each specified class for my $test_class_data (@test_input) { my ($test_class_name, @test_property_names) = @$test_class_data; # test clear with this class init($test_class_name, @test_property_names); clear(); my $state_after_first_init_and_clear_for_class = take_state_snapshot(); #$DB::single=1; is_deeply( $state_after_first_init_and_clear_for_class, $state_after_initial_clear, "clear returns restores state after init" ); init($test_class_name, @test_property_names); clear(); my $state_after_second_init_and_clear_for_class = take_state_snapshot(); is_deeply( $state_after_second_init_and_clear_for_class, $state_after_first_init_and_clear_for_class, "clear returns restores state after repeated init" ); # ensure we really are getting a different set of state snapshots # this really only needs to be done once, but requires init() clear(); init($test_class_name, @test_property_names); is(scalar(@transactions), $num_trans, "got the expected number of transactions for the test plan: $num_trans"); is(scalar(@transaction_prior_states), $num_trans, "got the expected number of state snapshots for the test plan: $num_trans"); # sanity check the structures against the plan my $matching_states_found = eval { for my $state_a ($state_initial, @transaction_prior_states,$state_final) { for my $state_b ($state_initial, @transaction_prior_states,$state_final) { next if $state_a == $state_b; if (Compare($state_a,$state_b)) { return 1; } } } return 0; }; ok(!$matching_states_found, "all state snapshots differ from each other"); # ensure we get the _same_ different set each init(). my @expected_states = @transaction_prior_states; clear(); init($test_class_name, @test_property_names); for my $n (0 .. $#transaction_prior_states) { my $expected = $expected_states[$n]; my $actual = $transaction_prior_states[$n]; #my $match = Compare($expected,$actual); #print "match is $match\n"; is_deeply($expected, $actual, "states match for snapshot $n") or diag(compare_snapshots($expected,$actual)); } # test rollback, finally # simple walk backward through transactions for (my $n = $num_trans-1; $n >= 0; $n--) { rollback_and_verify($n, " with later transactions already rolled-back on $test_class_name"); } # ensure rolling back multiple transactions works #for (my $n = 0; $n <= $num_trans; $n++) { for (my $n = $num_trans-1; $n >= 0; $n--) { clear(); init($test_class_name, @test_property_names); rollback_and_verify($n, " with later transactions forcibly rolled-back on $test_class_name"); } } sub compare_snapshots { my ($s1, $s2) = @_; my $f1 = "/tmp/t99-$$.f1"; my $f2 = "/tmp/t99-$$.f2"; IO::File->new(">$f1")->print(YAML::Dump($s1)); IO::File->new(">$f2")->print(YAML::Dump($s2)); #system "opendiff $f1 $f2"; return `sdiff -s $f1 $f2`; } 80_command_define_datasource.t000444023532023421 2562212121654173 20665 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use IO::File; use Test::More; my ($oracle,$postgres,$mysql); BEGIN { eval "use DBD::mysql"; eval "use DBD::Pg"; eval "use DBD::Oracle"; $oracle = $INC{"DBD/Oracle.pm"}; $mysql = $INC{"DBD/mysql.pm"}; $postgres = $INC{"DBD/Pg.pm"}; my $tests = 33; $tests += 12 if $oracle; $tests += 12 if $postgres; $tests += 12 if $mysql; plan tests => $tests; } BEGIN { use_ok('UR::Namespace::Command::Define::Datasource'); use_ok('UR::Namespace::Command::Define::Datasource::Sqlite'); use_ok('UR::Namespace::Command::Define::Datasource::Oracle'); use_ok('UR::Namespace::Command::Define::Datasource::Mysql'); use_ok('UR::Namespace::Command::Define::Datasource::Pg'); } my $data_source_dir = URT->get_base_directory_name() . '/DataSource/'; my @FILES_TO_DELETE = map { $data_source_dir . $_ } qw( TestcaseSqlite.pm TestcaseSqlite.sqlite3 TestcaseSqlite2.pm TestcaseOracle.pm TestcaseMysql.pm TestcasePg.pm ); push @FILES_TO_DELETE, '/tmp/TestcaseSqlite.sqlite3'; chdir $data_source_dir; my $cleanup_files = sub { unlink @FILES_TO_DELETE }; &$cleanup_files; UR::Namespace::Command::Define::Datasource->dump_status_messages(0); # don't print to the terminal # SQLite { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); ok($command->execute(),'Executed SQLite define'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite.sqlite3'; ok(-f $expected_path, 'Created SQLite database file'); $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite.pm'; ok(-f $expected_path, 'Created SQLite DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseSqlite/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseSqlite/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::SQLite/, "'is' line looks ok"); like($src, qr/sub server \{ \S+\/URT\/DataSource\/TestcaseSqlite.sqlite3/, 'server line looks ok'); unlike($src, qr/sub owner/, 'No owner line, as expected'); unlike($src, qr/sub login/, 'No login line, as expected'); unlike($src, qr/sub auth/, 'No auth line, as expected'); &$cleanup_files; } { my $db_file = '/tmp/TestcaseSqlite.sqlite3'; IO::File->new($db_file, 'w')->close(); my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite2 --server /tmp/TestcaseSqlite.sqlite3 ) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); ok($command->execute(),'Executed SQLite define'); my $expected_path = '/tmp/TestcaseSqlite.sqlite3'; ok(-f $expected_path, 'Created SQLite database file'); $expected_path = $command->namespace_path . '/DataSource/TestcaseSqlite2.pm'; ok(-f $expected_path, 'Created SQLite DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseSqlite/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseSqlite/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::SQLite/, "'is' line looks ok"); like($src, qr/sub server \{ '\/tmp\/TestcaseSqlite.sqlite3/, 'server line looks ok'); unlike($src, qr/sub owner/, 'No owner line, as expected'); unlike($src, qr/sub login/, 'No login line, as expected'); unlike($src, qr/sub auth/, 'No auth line, as expected'); # Don't remove the files because we want to test failure next } { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(sqlite --dsname TestcaseSqlite) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining SQLite DS'); $command->dump_error_messages(0); ok(! $command->execute(), 'Execute correctly returned failure'); my $message = $command->error_message; is($message,'A data source named URT::DataSource::TestcaseSqlite already exists', 'Error message mentions the target datasource module already exists'); &$cleanup_files; } # Oracle if($oracle) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(oracle --dsname TestcaseOracle --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Oracle DS'); open my $old_stderr, ">&STDERR"; close(STDERR); $command->dump_error_messages(0); # The execute() here will fail because TestcaseOracle isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Oracle define failed as expected'); like($@, qr/Failed to connect to the database/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseOracle.pm'; ok(-f $expected_path, 'Created Oracle DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseOracle/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseOracle/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::Oracle/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcaseOracle' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping Oracle tests since DBD::Oracle is not installed and configured"; } # PostgreSQL if ($postgres) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(pg --dsname TestcasePg --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Pg DS'); $command->dump_error_messages(0); open my $old_stderr, ">&STDERR"; close(STDERR); # The execute() here will fail because TestcasePg isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Pg define failed as expected'); like($@, qr/(Failed to connect to the database)|(Can't load \S+ for module DBD::Pg)/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcasePg.pm'; ok(-f $expected_path, 'Created Pg DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcasePg/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcasePg/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::Pg/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcasePg' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping PostgreSQL tests since DBD::pg is not installed"; } # MySQL if($mysql) { my($delegate_class, $create_params) = UR::Namespace::Command::Define::Datasource->resolve_class_and_params_for_argv( qw(mysql --dsname TestcaseMysql --owner foo --login me --auth passwd) ); ok($delegate_class, "Resolving parameters for define datasource, delegate class $delegate_class"); my $command = $delegate_class->create(%$create_params); ok($command,'Created command obj for defining Mysql DS'); $command->dump_error_messages(0); open my $old_stderr, ">&STDERR"; close(STDERR); # The execute() here will fail because TestcaseMysql isn't a real database # and the connection test at the end of the command will fail my $retval = eval { $command->execute() }; open STDERR, ">&", $old_stderr; ok(!$retval,'Executing Mysql define failed as expected'); like($@, qr/Failed to connect to the database/, 'Failure was because it could not connect to the database'); my $expected_path = $command->namespace_path . '/DataSource/TestcaseMysql.pm'; ok(-f $expected_path, 'Created Mysql DS module'); my $src = _read_file($expected_path); # Not an exhaustive syntax check, just look for some things like($src, qr/package URT::DataSource::TestcaseMysql/, 'package line looks ok'); like($src, qr/class URT::DataSource::TestcaseMysql/, 'class line looks ok'); like($src, qr/is.*UR::DataSource::MySQL/, "'is' line looks ok"); like($src, qr/sub server \{ 'TestcaseMysql' \}/, 'server line looks ok'); like($src, qr/sub owner \{ 'foo' \}/, 'owner line looks ok'); like($src, qr/sub login \{ 'me' \}/, 'login line looks ok'); like($src, qr/sub auth \{ 'passwd' \}/, 'auth line looks ok'); &$cleanup_files; } else { diag "skipping MySQL tests since DBD::mysql is not installed"; } sub _read_file { my $path = shift; my $fh = IO::File->new($path); die "Can't open $path: $!" unless $fh; # Read in the whole file local $/; undef $/; my $src = <$fh>; $fh->close(); return $src; } 66_nullable_hangoff_data.t000444023532023421 517512121654173 17767 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 6; &setup_classes_and_db(); my $thing = URT::Thing->create(thing_id => 3, name => 'Fred'); # There's now 1 colorless thing in the DB and one in the object cache my @colorless = URT::Thing->get(color => undef); is(scalar(@colorless), 2, 'Got two colorless things'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name) values (?,?)'); foreach my $row ( ( [1, 'Bob'], [2, 'Joe'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } ok($dbh->do('create table attributes (attr_id integer, thing_id integer, key varchar, value varchar)'), 'Created attributes table'); $insert = $dbh->prepare('insert into attributes (attr_id, thing_id, key, value) values (?,?,?,?)'); foreach my $row ( ( [1, 1, 'color', 'green'], [2, 1, 'address', '1234 Main St'], [3, 2, 'address', '2345 Oak St'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'attributes': $DBI::errstr"; } } $dbh->commit(); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, attributes => { is => 'URT::Attribute', reverse_as => 'thing', is_many => 1 }, color => { is => 'String', via => 'attributes', to => 'value', where => [key => 'color'], is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); ok(UR::Object::Type->define( class_name => 'URT::Attribute', id_by => 'attr_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, key => { is => 'String' }, value => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'attributes'), 'Created class URT::Attribute'); } 03e_params_list.t000444023532023421 531412121654173 16155 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 7; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'primary_car_color like' => 'red%', 'primary_car.engine.size' => [428,429], 'cars.color in' => ['red','blue'], ); my $bx2 = URT::Person->define_boolexpr( -or => [ [ 'is_cool' => 1, 'cars.color in' => ['red','blue'], ], [ 'primary_car_color like' => 'red%', 'primary_car.engine.size' => [428,429], ], ] ); for my $bx ($bx1, $bx2) { my @pa = $bx->params_list; my @pb = $bx->_params_list; my $bxa = URT::Person->define_boolexpr(@pa); is($bxa->id, $bx->id, "the params_list reconstructs the same object $bxa"); my $bxb = URT::Person->define_boolexpr(@pb); is($bxb->id, $bx->id, "the params_list reconstructs the same object $bxb"); } 21d_db_entity_observers.t000444023532023421 3322612121654173 17734 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 115; # Test that basic signals get fired off correctly for DB entities my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, rank integer)'), 'create person table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 1)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 2)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 3)"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 4)"), 'insert into person table'); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, rank => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); my @person_observations = (); my @person_ghost_observations = (); my @object1_observations = (); my @object2_observations = (); my @ghost1_observations = (); sub clear_observations { @person_observations = (); @person_ghost_observations = (); @object1_observations = (); @object2_observations = (); @ghost1_observations = (); } my $person_obv = URT::Person->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @person_observations, [$obj, $method, @other_args]; }); ok($person_obv, "made an observer on Person class"); # Observations on person1 won't fire after it's deleted because it's a ghost. Make a new # observer for the ghsot class my $person_ghost_obv = URT::Person::Ghost->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @person_ghost_observations, [$obj, $method, @other_args]; }); ok($person_ghost_obv, 'Make observer for URT::Person::Ghost class'); clear_observations(); my $person1 = URT::Person->get(1); ok($person1, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ [$person1, 'load'] ], # subclasses/loaded as Employee 'Person observations match expected'); @object1_observations = (); my $person1_obj_observer = $person1->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @object1_observations, [$obj,$method,@other_args]}); ok($person1_obj_observer, 'made an observer on person id 1'); clear_observations(); my $person2 = URT::Person->get(2); ok($person2, 'Got person ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'person object 1 observer saw no observations'); my $person2_obj_observer = $person2->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @object2_observations, [$obj,$method,@other_args]}); ok($person2_obj_observer, 'made an observer on person id 2'); # Call the rank mutator, but feed it the original value clear_observations(); my $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations from transaction creation'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(1), 'User rank mutator to set the same value'); is(scalar(@person_observations), 0, 'No Person observations from setting the same value'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from setting the same value'); is(scalar(@object2_observations), 0, 'No object 2 observations from setting the same value'); clear_observations(); ok($trans->rollback(), 'Rollback software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction rollback'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction rollback'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction rollback'); # Now set the rank to a new value clear_observations(); $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(2), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 1, 2] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 1, 2] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($trans = UR::Context::Transaction->rollback(), 'rollback'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 2, 1] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 2, 1] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Set the rank to a new value and commit the software transaction clear_observations(); $trans = UR::Context::Transaction->begin(); ok($trans, 'Begin software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction creation'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from transaction creation'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($person1->rank(2), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 1, 2] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 1, 2] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); clear_observations(); ok($trans = UR::Context::Transaction->commit(), 'Commit software transaction'); is(scalar(@person_observations), 0, 'No Person observations from transaction commit'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from transaction commit'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Now commit to the underlying context, with no-commit on ok(UR::DBI->no_commit(1), 'Turn on no-commit flag'); clear_observations(); ok(UR::Context->commit, 'Commit to the DB'); is(scalar(@person_observations), 0, 'No Person observations from Context commit with no_commit on'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object observations from Context commit with no_commit on'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); # Make another change, turn no-commit off, and try committing again clear_observations(); ok($person1->rank(3), 'Use rank mutator to change value'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'rank', 2, 3] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'rank', 2, 3] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction creation'); ok(! UR::DBI->no_commit(0), 'Turn off no-commit flag'); clear_observations(); ok(UR::Context->commit, 'Commit to the DB'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'commit'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'commit'] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from transaction commit'); # Delete person object 1, change person 2 and commit clear_observations(); ok($person1->delete, 'Delete person object 1'); my $person1_ghost = URT::Person::Ghost->get(1); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person1, 'delete'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 1, 'One Person ghost observations'); is_deeply(\@person_ghost_observations, [ [$person1_ghost, 'create'] ], 'Person ghost observations match expected'); is(scalar(@object1_observations), 1, 'One observation on person object'); is_deeply(\@object1_observations, [ [$person1, 'delete'] ], 'person object observations match expected'); is(scalar(@object2_observations), 0, 'No object 2 observations from delete'); my $object1_ghost_obv = $person1_ghost->add_observer(callback => sub { my $obj = shift; my $method = shift; my @other_args = @_; push @ghost1_observations, [$obj, $method, @other_args]; }); ok($object1_ghost_obv, 'Create observer for now-deleted Person object 1'); clear_observations(); ok($person2->rank(5), 'Change rank of person 2'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person2, 'rank', 2, 5] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 0, 'No Person ghost observations'); is(scalar(@object1_observations), 0, 'No object 1 observations'); is(scalar(@ghost1_observations), 0, 'No ghost 1 observations'); is(scalar(@object2_observations), 1, 'One observation on person object 2'); is_deeply(\@object2_observations, [ [$person2, 'rank', 2, 5] ], 'person 2 object observations match expected'); clear_observations(); ok(UR::Context->commit, 'Commit to DB'); is(scalar(@person_observations), 1, 'One observation on Person class'); is_deeply(\@person_observations, [ [$person2, 'commit'] ], 'Person observations match expected'); is(scalar(@person_ghost_observations), 1, 'One observation on Person Ghost class'); is_deeply(\@person_ghost_observations, [ [$person1_ghost, 'commit'] ], 'Person Ghost observations match expected'); is(scalar(@object1_observations), 0, 'No observations on person 1 object'); is(scalar(@ghost1_observations), 1, 'One observation on person 1 ghost object'); is_deeply(\@ghost1_observations, [ [$person1_ghost, 'commit'] ], 'person ighost object observations match expected'); is(scalar(@object2_observations), 1, 'One observation on person 2 object'); is_deeply(\@object2_observations, [ [$person2, 'commit'] ], 'person 2 object observations match expected'); 1; 96_context_clear_cache.t000444023532023421 416712121654173 17470 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 19; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?)'); foreach my $id ( 1..5 ) { $sth->execute($id,$id); } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); my $query_count = 0; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); $query_count = 0; my @things = URT::Thing->get(); is(scalar(@things), 5, 'Got all 5 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->is_loaded(); is(scalar(@things), 5, 'is_loaded returns all 5 things'); is($query_count, 0, 'Made no queries'); UR::Context->dump_warning_messages(0); ok(UR::Context->current->clear_cache(), 'clear cache'); $query_count = 0; @things = URT::Thing->is_loaded(); is(scalar(@things), 0, 'is_loaded now shows no things in memory'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 5, 'Got all 5 things'); is($query_count, 1, 'Made 1 query'); ok(UR::Context->current->clear_cache(), 'clear cache'); $query_count = 0; @things = URT::Thing->get('value <' => 3); is(scalar(@things), 2, 'Got 2 things with value < 3'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get('value >' => 3); is(scalar(@things), 2, 'Got 2 things with value > 3'); is($query_count, 1, 'Made 1 query'); ok(UR::Context->current->clear_cache(), 'clear cache'); my @things2 = URT::Thing->is_loaded(); is(scalar(@things2), 0, 'Still saw 0 things in memory'); #print Data::Dumper::Dumper(\@things); is(scalar(@things), 2, '2 objects are still held in the list'); isa_ok($_, 'UR::DeletedRef') foreach @things; 92_save_object_with_propertyless_column.t000444023532023421 402312121654173 23225 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # Make a class attached to a table where some columns in the table have # no associated property. Test that we can CRUD my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh->do("create table foo (foo_id integer NOT NULL PRIMARY KEY, name varchar, missing varchar)"), 'create table'); ok($dbh->do("insert into foo values (100,'DeleteMe', 'blah')"), 'insert row'); ok($dbh->do("insert into foo values (101,'UpdateMe', 'blah')"), 'insert row'); UR::Object::Type->define( class_name => 'URT::Foo', id_by => 'foo_id', has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'foo', ); my $obj = URT::Foo->get(name => 'DeleteMe'); ok($obj, 'Got an object'); ok($obj->delete(), 'Called delete()'); $obj = URT::Foo->get(name => 'UpdateMe'); ok($obj, 'Got a second object'); ok($obj->name('Updated'), 'Changed its name'); $obj = URT::Foo->create(name => 'Created'); ok($obj, 'Created an object'); my $new_object_id = $obj->id; my $commit = eval { UR::Context->commit() }; ok($commit, 'commit'); ok(! $@, 'No exceptions during commit'); diag($@) if $@; my @row = $dbh->selectrow_array('select foo_id,name,missing from foo where foo_id = 100'); ok(!scalar(@row), 'Deleted object was deleted from database'); @row = $dbh->selectrow_array('select foo_id,name,missing from foo where foo_id = 101'); ok(scalar(@row), 'Found row in database for updated object'); is($row[1], 'Updated', 'name column was updated correctly'); is($row[2], 'blah', 'missing column was not touched'); @row = $dbh->selectrow_array("select foo_id,name,missing from foo where foo_id = $new_object_id"); ok(scalar(@row), 'Found row in database for created object'); is($row[1], 'Created', 'name column is correct'); is($row[2], undef, 'missing column is correctly NULL/undef'); 95_normalize_property_description.t000444023532023421 437612121654173 22063 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use above 'UR'; use Test::More; my @desc = ( class_name => 'Foo', has_abstract_constant => [ subject_class_name => { is_abstract => 1, is_constant => 1 }, perspective => { is_abstract => 1, is_constant => 1 }, toolkit => { is_abstract => 1, is_constant => 1 }, ], has_optional => [ parent_view => { is => 'UR::Object::View', id_by => 'parent_view_id', doc => 'when nested inside another view, this references that view', }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being observed' }, aspects => { is => 'UR::Object::View::Aspect', reverse_as => 'parent_view', is_many => 1, specify_by => 'name', order_by => 'number', doc => 'the aspects of the subject this view renders' }, default_aspects => { is => 'ARRAY', is_abstract => 1, is_constant => 1, is_many => 1, # technically this is one "ARRAY" default_value => undef, doc => 'a tree of default aspect descriptions' }, ], has_optional_transient => [ _widget => { doc => 'the object native to the specified toolkit which does the actual visualization' }, _observer_data => { is => 'HASH', is_transient => 1, value => undef, # hashref set at construction time doc => ' hooks around the subject which monitor it for changes' }, ], has_many_optional => [ aspect_names => { via => 'aspects', to => 'name' }, ] ); my $class_name = "UR::Object::View"; my $new_desc = UR::Object::Type->_normalize_class_description(@desc); ok($new_desc, 'normalized class object'); my $new_desc2 = UR::Object::Type->_normalize_class_description(%$new_desc); ok($new_desc2, 'normalized class object again'); is_deeply($new_desc, $new_desc2, '2x normalization produces consistent answer') or diag Data::Dumper::Dumper($new_desc, $new_desc2); done_testing(); 98_ur_update.t000555023532023421 7423512121654173 15533 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; #BEGIN { $ENV{UR_CONTEXT_BASE} = "URT::Context::Testing" }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use DBI; use IO::Pipe; use Test::More; if ($^O eq 'darwin') { plan skip_all => 'known to fail OS X' } elsif ($INC{"UR.pm"} =~ /blib/) { plan skip_all => 'skip running during install', } else { plan tests => 87; } use UR::Namespace::Command::Update::ClassesFromDb; UR::DBI->no_commit(1); # This can only be run with the cwd at the top of the URT namespace require Cwd; my $namespace_dir = URT->get_base_directory_name; my $working_dir = Cwd::abs_path(); if ($working_dir ne $namespace_dir) { if (-d $namespace_dir) { chdir($namespace_dir); } else { die "Cannot determine URT's namespace directory, exiting"; } } cleanup_files(); sub cleanup_files { #unlink $sqlite_file; #$DB::single = 1; my $namespace_dir = URT->get_base_directory_name; for my $filename ( qw| Car.pm Employee.pm Person.pm .deleted/Car.pm .deleted/Employee.pm .deleted/Person.pm | ) { if (-e "$namespace_dir/$filename") { #warn "unlinking $filename\n"; unlink "$namespace_dir/$filename"; } } } UR::Namespace::Command::Update::ClassesFromDb->dump_error_messages(1); UR::Namespace::Command::Update::ClassesFromDb->dump_warning_messages(1); UR::Namespace::Command::Update::ClassesFromDb->dump_status_messages(0); UR::Namespace::Command::Update::ClassesFromDb->status_messages_callback( sub { my $self = shift; my $msg = shift; print " $msg\n"; return 1; } ); # We launch a similar command multiple times. my($delegate_class,$create_params) = UR::Namespace::Command::Update::ClassesFromDb->resolve_class_and_params_for_argv(qw(--data-source URT::DataSource::SomeSQLite)); ok($delegate_class, "Resolving parameters for update: class is $delegate_class"); my $command_obj = sub { my $command_obj = $delegate_class->create(%$create_params, _override_no_commit_for_filesystem_items => 1); ok($command_obj, "created new command for " . join(" ", @_)); ok($command_obj->execute(), "executed new command for " . join(" ",@_)); return $command_obj->result; }; bless ($command_obj,"DummyExecutor"); sub DummyExecutor::execute { shift->(@_); } ok($command_obj, "Created a dummy command object for updating the classes"); my $ds_class = 'URT::DataSource::SomeSQLite'; # The datasource we'll be making tables in $ds_class->class; my $dbh = $ds_class->get_default_handle(); ok($dbh, 'Got database handle'); # This wrapper to get_changes filters out things like the command-line parameters # until that bug is fixed. my $trans; sub get_changes { my @changes = grep { $_->changed_class_name ne 'UR::Namespace::Command::Update::ClassesFromDb' } grep { $_->changed_class_name ne "UR::Namespace::CommandParam" } grep { $_->changed_class_name ne 'UR::DataSource::Meta' && substr($_->changed_aspect,0,1) ne '_'} grep { $_->changed_aspect ne 'query' } $trans->get_change_summary(); return @changes; } sub cached_dd_objects { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); } sub cached_dd_object_count { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); return scalar(@obj); } sub cached_class_object_count { my $cx = UR::Context->current; my @obj = grep { ref($_) =~ /UR::Object::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); return scalar(@obj); } sub cached_person_dd_objects { my $cx = UR::Context->current; my @obj = grep { $_->{table_name} eq "person" } grep { ref($_) =~ /::DB::/ } $cx->all_objects_loaded('UR::Object'), $cx->all_objects_loaded('UR::Object::Ghost'); } sub cached_person_summary { my @obj = map { ref($_) . "\t" . $_->{id} } cached_person_dd_objects(); return @obj; } sub undo_log_summary { my @c = do { no warnings; reverse @UR::Context::Transaction::change_log; }; return map { $_->{changed_class_name} . "\t" . $_->{changed_id} . "\t" . $_->{changed_aspect} } grep { not ($_->{changed_class_name} =~ /^UR::Object/ and $_->{changed_aspect} eq "load") } @c; } # Hack - These get filled in at the bottom initialize_check_changes_data_structure() our($check_changes_1, $check_changes_2, $check_changes_3); # Empty schema $trans = UR::Context::Transaction->begin(); ok($trans, "began transaction"); ok($command_obj->execute(),'Executing update on an empty schema'); my @changes = get_changes(); is(scalar(@changes),0, "zero changes for an empty schema"); # note this for comparison in future tests. my $expected_dd_object_count = cached_dd_object_count(); # don't rollback # Make a table ok($dbh->do('CREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, name varchar)'), 'Create person table'); $trans = UR::Context::Transaction->begin(); ok($trans, "CREATED PERSON and began transaction"); ok($command_obj->execute(),'Executing update after creating person table'); initialize_check_change_data_structures(); @changes = get_changes(); # FIXME The test should probably break out each type of changed thing and check # that the counts of each type are correct, and not just the count of all changes my $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_1, "Change list is correct"); my $personclass = UR::Object::Type->get('URT::Person'); isa_ok($personclass, 'UR::Object::Type'); # FIXME why isn't this a UR::Object::Type ok($personclass->module_source_lines, 'Person class module has at least one line'); is($personclass->class_name, 'URT::Person', 'Person class class_name is correct'); is($personclass->table_name, 'person', 'Person class table_name is correct'); is($UR::Context::current->resolve_data_sources_for_class_meta_and_rule($personclass)->id, $ds_class, 'Person class data_source is correct'); is_deeply([sort $personclass->direct_column_names], ['name','person_id'], 'Person object has all the right columns'); is_deeply([$personclass->direct_id_column_names], ['person_id'], 'Person object has all the right id column names'); # Another test case should make sure the other class introspection methods like inherited_property_names, # all_table_names, etc work correctly for all kinds of objects my $module_path = $personclass->module_path; ok($module_path, "got a module path"); ok(-f $module_path, 'Person.pm module exists'); ok(! UR::Object::Type->get('URT::NonExistantClass'), 'Correctly cannot load a non-existant class'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Make the employee and car tables refer to person, and add a column to person ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY CONSTRAINT fk_person_id REFERENCES person(person_id), rank integer)'), 'Employee inherits from Person'); ok($dbh->do('ALTER TABLE person ADD COLUMN postal_address varchar'), 'Add column to Person'); ok($dbh->do('CREATE TABLE car (car_id integer NOT NULL PRIMARY KEY, owner_id integer NOT NULL CONSTRAINT fk_person_id2 REFERENCES person(person_id), make varchar, model varchar, color varchar, cost number)'), 'Create car table'); $trans = UR::Context::Transaction->begin(); ok($trans, "CREATED EMPLOYEE AND CAR AND UPDATED PERSON and began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_2, "Change list is correct"); # Verify the Person.pm and Employee.pm modules exist $personclass = UR::Object::Type->get('URT::Person'); ok($personclass, 'Person class loaded'); my %got = map { $_ => 1} $personclass->direct_column_names; is_deeply(\%got, { name => 1, person_id => 1, postal_address => 1 }, 'Person object has all the right columns'); %got = map { $_ => 1 } $personclass->all_property_names; is_deeply(\%got, { name => 1, person_id => 1, postal_address => 1 }, 'Person object has all the right properties'); %got = map { $_ => 1 } $personclass->direct_id_column_names; is_deeply(\%got, { person_id => 1 }, 'Person object has all the right id column names'); my $employeeclass = UR::Object::Type->get('URT::Employee'); ok($employeeclass, 'Employee class loaded'); isa_ok($employeeclass, 'UR::Object::Type'); # There is no standardized way to spot inheritance from the schema. # The developer can reclassify in the source, and subsequent updates would respect it. # FIXME: test for this. # What about if one class' primary keys are all foreign keys to all of another class' primary keys? # FIXME - what about foreign keys not involving primary keys? Make object accessor properties ok(! $employeeclass->isa('URT::Car'), 'Employee class is correctly not a Car'); ok($employeeclass->module_source_lines, 'Employee class module has at least one line'); %got = map { $_ => 1 } $employeeclass->direct_column_names; is_deeply(\%got, { employee_id => 1, rank => 1 }, 'Employee object has all the right columns'); %got = map { $_ => 1 } $employeeclass->all_property_names; is_deeply(\%got, { employee_id => 1, person_employee => 1, rank => 1 }, 'Employee object has all the right properties'); %got = map { $_ => 1 } $employeeclass->direct_id_column_names; is_deeply(\%got, { employee_id => 1 }, 'Employee object has all the right id column names'); ok($employeeclass->table_name eq 'employee', 'URT::Employee object comes from the employee table'); my $carclass = UR::Object::Type->get('URT::Car'); ok($carclass, 'Car class loaded'); is($carclass->class_name,'URT::Car', "class name is set correctly"); isa_ok($carclass,'UR::Object::Type'); ok(! $carclass->class_name->isa('URT::Person'), 'Car class is correctly not a Person'); %got = map { $_ => 1 } $carclass->direct_column_names; is_deeply(\%got, { car_id => 1, color => 1, cost => 1, make => 1, model => 1, owner_id => 1 }, 'Car object has all the right columns'); %got = map { $_ => 1 } $carclass->all_property_names; is_deeply(\%got, { car_id => 1, color => 1, cost => 1, make => 1, model => 1, owner_id => 1, person_owner => 1 }, 'Car object has all the right properties'); %got = map { $_ => 1 } $carclass->direct_id_column_names; is_deeply(\%got, { car_id => 1 }, 'Car object has all the right id column names'); ok($carclass->table_name eq 'car', 'Car object comes from the car table'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Drop a table ok($dbh->do('DROP TABLE car'),'Removed Car table'); $trans = UR::Context::Transaction->begin(); ok($trans, "DROPPED CAR and began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); $changes_as_hash = convert_change_list_for_checking(@changes); is_deeply($changes_as_hash, $check_changes_3, "Change list is correct"); ok($personclass = UR::Object::Type->get('URT::Person'),'Loaded Person class'); ok($employeeclass = UR::Object::Type->get('URT::Employee'), 'Loaded Employee class'); $carclass = UR::Object::Type->get('URT::Car'); ok(!$carclass, 'Car class is correctly not loaded'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Drop a constraint # SQLite doesn't support altering a table to drop a constraint, so we need to # drop the table and recreate it without the constraint #ok($dbh->do('DROP TABLE employee'), 'Temporarily dropping table employee'); #ok($dbh->do('CREATE TABLE employee (employee_id integer NOT NULL PRIMARY KEY, rank integer)'), 'Recreate Employee without constraint'); #$trans = UR::Context::Transaction->begin(); #ok($trans, "Changed EMPLOYEE and began transaction"); # # ok($command_obj->execute(), 'Updating schema'); # my @o = UR::DataSource::RDBMS::FkConstraint->get(namespace => 'URT'); #print "\n\n*** Got ",scalar(@o)," FkConstraints\n"; # @changes = get_changes(); # $changes_as_hash = convert_change_list_for_checking(@changes); # # Drop the other two tables ok($dbh->do('DROP TABLE employee'),'Removed employee table'); ok($dbh->do('DROP TABLE person'),'Removed person table'); ok($dbh->do('CREATE TABLE person (person_id integer NOT NULL PRIMARY KEY, postal_address varchar)'), 'Replaced table person w/o column "name".'); #ok($dbh->do('ALTER TABLE person DROP column name'),'Removed the name column from the person table'); ##begin(); ok($trans, "DROPPED EMPLOYEE AND UPDATED PERSON began transaction"); ok($command_obj->execute(), 'Updating schema'); @changes = get_changes(); is(scalar(@changes), 15, "found changes for two more dropped tables"); $trans = UR::Context::Transaction->begin(); ok($trans, "Restarted transaction since some data is not really sync'd at sync_filesystem"); ok($command_obj->execute(), 'Updating schema anew.'); ok(! UR::Object::Type->get('URT::Employee'), 'Correctly could not load Employee class'); ok(! UR::Object::Type->get('URT::Car'),'Correctly could not load Car class'); $personclass = UR::Object::Type->get('URT::Person'); unless ($personclass) { #$DB::single = 1; } $personclass->ungenerate; #$DB::single = 1; $personclass->generate; ok($personclass, 'Person class loaded'); is_deeply([sort $personclass->direct_column_names], ['person_id','postal_address'], 'Person object has all the right columns'); is_deeply([sort $personclass->class_name->__meta__->all_property_names], ['person_id','postal_address'], 'Person object has all the right properties'); is_deeply([$personclass->direct_id_column_names], ['person_id'], 'Person object has all the right id column names'); $trans->rollback; ok($trans->isa("UR::DeletedRef"), "rolled-back transaction"); is(cached_dd_object_count(), $expected_dd_object_count, "no data dictionary objects cached after rollback"); # Clean up after now-defunct class module files and SQLIte DB file cleanup_files(); sub child_db_interaction { my $dbfile = shift; my $pid; my $result = IO::Pipe->new(); my $to_child = IO::Pipe->new(); if ($pid = fork()) { $to_child->writer; $to_child->autoflush(1); $result->reader(); my @commands = map {$_ . "\n"} @_; foreach my $cmd ( @commands ) { $to_child->print($cmd); my $result = $result->getline(); chomp($result); my($retval,$string,$dbierr) = split(';',$result); return undef unless $retval; } $to_child->print("exit\n"); waitpid($pid,0); return 1; } else { $to_child->reader(); $result->writer(); $result->autoflush(1); my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","",""); unless ($dbh) { $result->print("0;can't connect;$DBI::errstr\n"); exit(1); } while(my $sql = $to_child->getline()) { chomp($sql); last if ($sql eq 'exit' || !$sql); my $sth = $dbh->prepare($sql); unless ($sth) { $result->print("0;prepare failed;$DBI::errstr\n"); $result->print("0;prepare failed;$DBI::errstr\n"); next; } my $retval = $sth->execute(); if ($retval) { $result->print($retval . "\n"); } else { $result->print("0;execute failed;$DBI::errstr\n"); } } $dbh->commit(); exit(0); } # end child } # Convert the list of changes to a data structure matching the expected changes sub convert_change_list_for_checking { my(@changes_list) = @_; my $changes = {}; foreach my $change ( @changes_list ) { my $changed_class_name = $change->{'changed_class_name'}; my $changed_id = $change->{'changed_id'}; my $changed_aspect = $change->{'changed_aspect'}; next if $changed_aspect eq 'query'; my $undo_data = $change->{'undo_data'}; if (exists $changes->{$changed_class_name}->{$changed_id}->{$changed_aspect}) { die "Two types of changes for the same thing in the same transaction!?"; } $changes->{$changed_class_name}->{$changed_id}->{$changed_aspect} = defined($undo_data); } return $changes; } # These expected change data structures work like this: # Based on the UR::Change objects, the first level hash key is # the changed_class_name, second level key is the changed_id, # the third level key is the changed_aspect, and the final value # is whether the undo_data is defined or not. # # Note that because of the way this testcase works, by creating a # transaction, updating metadata, and the rolling back the transaction, # all these metadata objects are always "created" as new, and not # modifications of existing things. # # There should probably be tests for the usual case where you would be # updating existing classes based on DB changes # Changes after creating the person table and running ur update classes sub initialize_check_change_data_structures { my $sqlite_owner = 'main'; $check_changes_1 = { 'UR::DataSource::RDBMS::Table' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson" => { create => '', # Meta DB object is created for the person table er_type => '', # And ur update classes fills in an er_type }, }, 'UR::DataSource::RDBMS::TableColumn' => { # The (new) person table has 2 (new) columns, person_id and name "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id" => { create => '' }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tname" => { create => '' }, }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { # person_id is the first and only primary column constraint "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id\t1" => { create => '' }, }, 'UR::Object::Type' => { # A new metaclass gets created for the person table 'URT::Person::Type' => { create => '' }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', # And then the class that goes with the table rewrite_module_header => 1, # And a record that we wrote a perl module on the filesystem }, }, 'UR::Object::Property' => { # Two new properties for the person class, name and person_id "URT::Person\tname" => { create => '' }, "URT::Person\tperson_id" => { create => '', is_id => '', # Created as an ID property }, }, }; # Changes after creating the car and employee tables, and adding postal_address column to person $check_changes_2 = { 'UR::DataSource::RDBMS::Table' => { # 3 tables: person, employee and car "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar" => { create => '', er_type => '', }, }, 'UR::DataSource::RDBMS::TableColumn' => { # Table person now has 3 columns: person_id, name and postal_address "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tname" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tpostal_address" => { create => '', }, # table employee has 2 columns: employee_id and rank "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\trank" => { create => '', }, # table car has these columns: car_id, make, model, color and cost "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tcar_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tmake" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tmodel" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tcolor" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tcost" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\towner_id" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraint' => { # Both employee and car tables have foreign keys to person "URT::DataSource::SomeSQLite\t$sqlite_owner\t$sqlite_owner\temployee\tperson\tfk_person_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\t$sqlite_owner\tcar\tperson\tfk_person_id2" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraintColumn' => { # The employee table FK points from employee_id to person_id "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\tfk_person_id\temployee_id" => { create => '', }, # The car table FK points from owner_id to person_id "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tfk_person_id2\towner_id" => { create => '', } }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { # All three tables have PK constraints for their ID columns "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tcar\tcar_id\t1" => { create => '', }, }, # running ur update classes makes 3 new classes 'UR::Object::Type' => { "URT::Car::Type" => { create => '', }, "URT::Employee::Type" => { create => '', }, "URT::Person::Type" => { create => '', }, }, # Each class has a property for the respective tablecolumn 'UR::Object::Property' => { "URT::Car\tcar_id" => { create => '', is_id => '', }, "URT::Car\tcolor" => { create => '', }, "URT::Car\tcost" => { create => '', }, "URT::Car\tmake" => { create => '', }, "URT::Car\tmodel" => { create => '', }, "URT::Car\towner_id" => { create => '', }, "URT::Car\tperson_owner" => { create => '', }, "URT::Employee\temployee_id" => { create => '', is_id => '', }, "URT::Employee\trank" => { create => '', }, "URT::Employee\tperson_employee" => { create => '', }, "URT::Person\tname" => { create => '', }, "URT::Person\tperson_id" => { create => '', is_id => '', }, "URT::Person\tpostal_address" => { create => '', }, }, # There a record of creating an instance of each class, and # that we wrote a perl module on the filesystem 'URT::Car::Type' => { 'URT::Car' => { create => '', rewrite_module_header => 1, }, }, 'URT::Employee::Type' => { 'URT::Employee' => { create => '', rewrite_module_header => 1, }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', rewrite_module_header => 1, }, }, # Because we rolled back the previous transaction, the old metadata # objects became ghosts. This is suboptimal and makes little sense # but there it is... 'UR::DataSource::RDBMS::Table::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson" => { delete => 1, }, }, 'UR::DataSource::RDBMS::TableColumn::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tname" => { delete => 1, }, }, 'UR::DataSource::RDBMS::PkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id\t1" => { delete => 1, }, }, }; # After removing the car table $check_changes_3 = { # FIXME Why are there no ghost objects for the dropped car stuff? 'UR::DataSource::RDBMS::Table' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee" => { create => '', er_type => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson" => { create => '', er_type => '', }, }, 'UR::DataSource::RDBMS::TableColumn' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\trank" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tname" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tpostal_address" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraint' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\t$sqlite_owner\temployee\tperson\tfk_person_id" => { create => '', }, }, 'UR::DataSource::RDBMS::FkConstraintColumn' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\tfk_person_id\temployee_id" => { create => '', }, }, 'UR::DataSource::RDBMS::PkConstraintColumn' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id\t1" => { create => '', }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id\t1" => { create => '', }, }, 'URT::Employee::Type' => { 'URT::Employee' => { create => '', rewrite_module_header => 1, }, }, 'URT::Person::Type' => { 'URT::Person' => { create => '', rewrite_module_header => 1, }, }, 'UR::Object::Type' => { 'URT::Person::Type' => { create => '', }, 'URT::Employee::Type' => { create => '', }, }, 'UR::Object::Property' => { "URT::Employee\temployee_id" => { create => '', is_id => '', }, "URT::Employee\trank" => { create => '', }, "URT::Employee\tperson_employee" => { create => '', }, "URT::Person\tperson_id" => { create => '', is_id => '', }, "URT::Person\tname" => { create => '', }, "URT::Person\tpostal_address" => { create => '', }, }, 'UR::DataSource::RDBMS::Table::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee" => { delete => 1, }, }, 'UR::DataSource::RDBMS::TableColumn::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\trank" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tname" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tpostal_address" => { delete => 1, }, }, 'UR::DataSource::RDBMS::FkConstraint::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\t$sqlite_owner\temployee\tperson\tfk_person_id" => { delete => 1, }, }, 'UR::DataSource::RDBMS::FkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\tfk_person_id\temployee_id" => { delete => 1, }, }, 'UR::DataSource::RDBMS::PkConstraintColumn::Ghost' => { "URT::DataSource::SomeSQLite\t$sqlite_owner\temployee\temployee_id\t1" => { delete => 1, }, "URT::DataSource::SomeSQLite\t$sqlite_owner\tperson\tperson_id\t1" => { delete => 1, }, }, }; } 1; 78_get_by_subclass_params_load_properly.t000444023532023421 1013512121654173 23171 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; # This tests a get() by subclass specific parameters on a subclass with no table of its own. # The idea is to make sure that queries run with any subclass specific parameters (which can # be stored in hangoff tables or calculated) do not cause the cache to believe it had loaded # more objects of that specific subclass than it actually has. setup_classes_and_db(); my $fido = URT::Dog->get(color => 'black'); ok($fido, 'Got fido by hangoff parameter (color)'); is($fido->name, 'fido', 'Fido has correct name'); is($fido->id, 1, 'Fido has correct id'); my $rex = URT::Dog->get(color => 'brown'); ok($rex, 'Got rex by hangoff parameter (color)'); SKIP: { skip 'Failed to get rex, not testing his properties', 2 if !defined $rex; is($rex->name, 'rex', 'Rex has correct name'); is($rex->id, 2, 'Rex has correct id'); }; $fido = URT::Dog->get(tag_id => 1); ok($fido, 'Got fido by calculated property (tag_id)'); is($fido->name, 'fido', 'Fido has correct name'); is($fido->id, 1, 'Fido has correct id'); $rex = URT::Dog->get(tag_id => 2); ok($rex, 'Got rex by calculated property (tag_id)'); SKIP: { skip 'Failed to get rex, not testing his properties', 2 if !defined $rex; is($rex->name, 'rex', 'Rex has correct name'); is($rex->id, 2, 'Rex has correct id'); }; sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do(q{ create table animal ( animal_id integer, name varchar, subclass varchar)}), 'Created animal table'); ok($dbh->do(q{ create table animal_param ( animal_param_id integer, animal_id integer references animal(animal_id), param_name varchar, param_value varchar)}), 'Created animal_param table'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (1,'fido','URT::Dog')"), 'Inserted fido'); ok($dbh->do("insert into animal_param (animal_param_id, animal_id, param_name, param_value) values (1, 1, 'color', 'black')"), 'Turned fido black'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (2,'rex','URT::Dog')"), 'Inserted rex'); ok($dbh->do("insert into animal_param (animal_param_id, animal_id, param_name, param_value) values (2, 2, 'color', 'brown')"), 'Turned rex brown'); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Animal', id_by => [ animal_id => { is => 'NUMBER', len => 10 }, ], has => [ name => { is => 'Text' }, subclass => { is => 'Text' }, ], has_many_optional => [ params => { is => 'URT::AnimalParam', reverse_as => 'animal', }, ], is_abstract => 1, subclassify_by => 'subclass', data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Dog', is => 'URT::Animal', has => [ tag_id => { calculate_from => [ 'animal_id' ], calculate => q{ return $animal_id; }, }, color => { via => 'params', is => 'Text', to => 'param_value', where => [ param_name => 'color', ], }, ], ); UR::Object::Type->define( class_name => 'URT::AnimalParam', id_by => [ animal_param_id => { is => 'NUMBER' }, ], has => [ animal => { id_by => 'animal_id', is => 'URT::Animal' }, param_name => { is => 'Text' }, param_value => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal_param', ); } 04a_sqlite_sync_database.t000444023532023421 1172412121654173 20037 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 30; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); # Make 3 tables, one with lower case names, one with upper, one with mixed case # do some CRUD and then commit(). Make sure the real data got saved and the # metadata is created correctly ok($dbh->do('create table person (person_id integer NOT NULL PRIMARY KEY, name varchar)'), 'create person table'); ok($dbh->do('create table EMPLOYEE (EMPLOYEE_ID integer NOT NULL PRIMARY KEY references person(person_id), OFFICE varchar)'), 'create EMPLOYEE table'); ok($dbh->do('create table InvenTory (InvenToryId integer NOT NULL PRIMARY KEY, Owner integer references EMPLOYEE(EMPLOYEE_ID), Name varchar)'), 'create InvenTory table'); # insert some data ok($dbh->do("insert into person values (100, 'UpdateName')"), 'insert person'); ok($dbh->do("insert into person values (101, 'DoNotChange')"), 'insert person'); ok($dbh->do("insert into person values (102, 'DeleteName')"), 'insert person'); ok($dbh->do("insert into person values (103, 'GetByJoin')"), 'insert person'); ok($dbh->do("insert into EMPLOYEE values (100, 'office 100')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (101, 'office 101')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (102, 'office 102')"), 'insert EMPLOYEE'); ok($dbh->do("insert into EMPLOYEE values (103, 'GetByJoin')"), 'insert person'); # person ID 100 has a black car and a red stapler # person ID 101 has a green chair and green phone # person ID 102 has nothing to begin with # person ID 103 has an item called 'Join' ok($dbh->do("insert into InvenTory values (100, 100, 'black car')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (101, 100, 'red stapler')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (102, 101, 'greep chair')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (103, 101, 'green phone')"), 'insert InvenTory'); ok($dbh->do("insert into InvenTory values (104, 103, 'Join')"), 'insert InvenTory'); # And now class definitions for those 3 tables UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', is_abstract => 1, id_by => 'person_id', has => ['name'], ); UR::Object::Type->define( class_name => 'URT::Inventory', data_source => 'URT::DataSource::SomeSQLite', table_name => 'InvenTory', id_by => 'InvenToryId', has => [ owner_id => { is => 'Integer', column_name => 'Owner' }, owner => { is => 'URT::Employee', id_by => 'owner_id' }, name => { is => 'String', column_name => 'Name' }, ], ); UR::Object::Type->define( class_name => 'URT::Employee', data_source => 'URT::DataSource::SomeSQLite', table_name => 'EMPLOYEE', is => 'URT::Person', id_by => 'EMPLOYEE_ID', has => [ office => { is => 'String', column_name => 'OFFICE' }, inventory => { is => 'URT::Inventory', reverse_as => 'owner', is_many => 1 }, ], ); my @sql = (); URT::DataSource::SomeSQLite->add_observer( aspect => 'query', callback => sub { my($data_source, $method, $sql) = @_; if ($method eq 'query') { $sql =~ s/^\s+|\s+$//g; # remove leading and trailing whitespace $sql =~ s/\s+/ /g; # change whitespace to a single space push(@sql, $sql); } } ); @sql = (); my $person = URT::Employee->get(name => 'NotThere'); ok(!$person, 'Get employee by name failed for non-existent name'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select EMPLOYEE.EMPLOYEE_ID, EMPLOYEE.OFFICE, person.name, person.person_id from EMPLOYEE INNER join person on EMPLOYEE.EMPLOYEE_ID = person.person_id where person.name = ? order by EMPLOYEE.EMPLOYEE_ID', 'SQL is correct'); @sql = (); $person = URT::Employee->get(name => 'UpdateName'); ok($person, 'Get employee by name worked'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select EMPLOYEE.EMPLOYEE_ID, EMPLOYEE.OFFICE, person.name, person.person_id from EMPLOYEE INNER join person on EMPLOYEE.EMPLOYEE_ID = person.person_id where person.name = ? order by EMPLOYEE.EMPLOYEE_ID', 'SQL is correct'); @sql = (); ok($person->name('Changed'), 'Change name for person'); is(scalar(@sql), 0, 'Made no queries'); @sql = (); my @inventory = $person->inventory(); is(scalar(@inventory), 2, 'That person has 2 inventory items'); is(scalar(@sql), 1, 'Made 1 query'); is($sql[0], 'select InvenTory.InvenToryId, InvenTory.Name, InvenTory.Owner from InvenTory where InvenTory.Owner = ? order by InvenTory.InvenToryId', 'SQL is correct'); @sql = (); $person = URT::Employee->get(name => 'DeleteName'); ok($person, 'Got Employee by name'); 49e_complicated_get_joining_through_view2.t000444023532023421 1150212121654173 23401 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - the final property/column we're filtering on is on the parent class of the delegation # - All the "tables" involved are really inline views &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_name => 'Collie'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL, do_include integer)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL, do_include integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id), do_include integer)"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id), do_include integer)"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name, do_include) values (?,?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart, do_include) values (?,?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id, do_include) values (?,?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id, do_include) values (?,?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie',1); $breed_insert->execute(1,1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie',1); $animal_insert->execute(2, 1,1); # a person named Jeff $name_insert->execute(3, 'Jeff',1); $person_insert->execute(3,2,1); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from named_thing where do_include = 1) named_thing_view', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from breed where do_include = 1) breed_view', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from animal where do_include = 1) animal_view', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from person where do_include = 1) person_view', ); } 11c_create_with_via_property.t000444023532023421 510712121654173 20735 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 20; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; UR::Object::Type->define( class_name => 'URT::Office', id_by => 'office_id', has => ['office_number'], ); UR::Object::Type->define( class_name => 'URT::Boss', id_by => 'boss_id', has => [ name => { is => 'Text' }, office => { is => 'URT::Office', id_by => 'office_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ name => { is => 'Text' }, boss => { is => 'URT::Boss', id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'name' }, boss_office => { is => 'URT::Office', via => 'boss', to => 'office' }, boss_office_number => { via => 'boss_office', to => 'office_number' }, ], ); my $o = URT::Office->create(office_number => 123); ok($o, 'Created office 123'); my $b = URT::Boss->create(name => 'Montgomery', office => $o); ok($b, 'Created boss with an office'); is($b->office_id, $o->id, 'Boss office_id is correct'); is($b->office, $o, 'Boss office is correct'); my $e = URT::Employee->create(name => 'Homer', boss => $b); ok($e, 'Created an employee with a boss'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); my $bx = URT::Employee->define_boolexpr(name => 'Mindy', boss_name => 'Montgomery'); ok($bx, 'Created BoolExpr with an Employee name and boss_name'); $bx = URT::Employee->define_boolexpr(name => 'Mindy', boss_office => $o); ok($bx, 'Created BoolExpr with an Employee name and boss_office'); $e = URT::Employee->create(name => 'Lenny', boss_office => $o); ok($e, 'Created an employee with a boss_office'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); $e = URT::Employee->create(name => 'Carl', boss => $b, boss_office => $o); ok($e, 'Created an employee with a consistent boss and boss_office'); is($e->boss_id, $b->id, 'Employee boss_id is correct'); is($e->boss, $b, 'Employee boss is correct'); is($e->boss_office, $o, 'Employee boss_office is correct'); my $o2 = URT::Office->create(office_number => 456); ok($o2, 'Created office 456'); $e = eval { URT::Employee->create(name => 'Frank', boss => $b, boss_office => $o2) }; ok(!$e, 'Correctly couldn not create an employee with conflicting boss and boss_office'); 97_used_libs.t000444023532023421 356612121654173 15467 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More; require UR::Util; require Cwd; require File::Temp; { local @INC = ('/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, []), 'no used_libs'); } { local @INC = ('/foo'); local $ENV{PERL5LIB} = ''; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'empty PERL5LIB'); } { local @INC = ('/foo', '/bar', '/baz'); local $ENV{PERL5LIB} = '/bar:/baz'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'multiple dirs in PERL5LIB'); } { local @INC = ('/foo', '/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'only one item in PERL5LIB (no trailing colon)'); } { local @INC = ('/foo', '/bar', '/baz'); local $ENV{PERL5LIB} = '/bar/:/baz'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'first dir in PERL5LIB ends with slash (@INC may not have slash)'); } { local @INC = ('/foo', '/foo', '/bar'); local $ENV{PERL5LIB} = '/bar'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'remove duplicate elements from used_libs'); } { local @INC = ('/foo'); local $ENV{PERL5LIB} = ''; local $ENV{PERL_USED_ABOVE} = '/foo/'; my @used_libs = UR::Util::used_libs(); ok(eq_array(\@used_libs, ['/foo']), 'remove trailing slash from used_libs'); } { my $orig_dir = Cwd::cwd(); my $temp_dir = File::Temp::tempdir(CLEANUP => 1); $DB::single = 1; my @pre_chdir_used_libs = UR::Util::used_libs(); chdir($temp_dir); $DB::single = 1; my @post_chdir_used_libs = UR::Util::used_libs(); chdir($orig_dir); is_deeply(\@pre_chdir_used_libs, \@post_chdir_used_libs, 'used_libs returns same libs after chdir'); } done_testing(); 03d_rule_construction.t000444023532023421 1744512121654173 17447 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 162; use Data::Dumper; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "Number", is_optional => 1 }, # These are designed to be similar to things stripped out of BoolExpr keys during resolve() is_id_only => { is => 'Boolean' }, some_param_key => { is => 'Text' }, a_unique_string => { is => 'Text' }, clobber__get_serial_number => { is => 'Number'}, the_change_count => { is => 'Number' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { id_by => [ ui_id => { is => 'Integer' }, ], has => [ name => { is => "String" }, group => { is => "String" }, ], }; my $test_obj = URT::Item->create(name => 'blah', group => 'cool', foo => 'foo', bar => 12345); foreach my $class_name ( qw( URT::Item URT::FancyItem ) ) { foreach my $meta_params ( [], [-group_by => ['bar']] ) { my $bx = $class_name->define_boolexpr(@$meta_params); my $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no filters is not is_partial_id'); ok($tmpl->matches_all, 'Rule template with no filters is matches_all'); $bx = $class_name->define_boolexpr(name => 'blah', @$meta_params); $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with one ID property filter is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with one ID property filter is not is_id_only'); ok($tmpl->is_partial_id, 'Rule template with one ID property filter is is_partial_id'); ok(!$tmpl->matches_all, 'Rule template with one ID property filter is not matches_all'); $bx = $class_name->define_boolexpr(name => 'blah', group => 'foo', @$meta_params); $tmpl = $bx->template; ok($bx->is_id_only, 'Rule with both ID property filters is is_id_only'); ok($tmpl->is_id_only, 'Rule template with both ID property filters is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with both ID property filter is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with both ID property filter is not matches_all'); $bx = $class_name->define_boolexpr(parent_name => '12345', @$meta_params); $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no ID filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no ID filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no ID filters is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with no ID filters is not matches_all'); } } foreach my $meta_params ( [], [-group_by => ['group']] ) { my $bx = URT::UnrelatedItem->define_boolexpr(@$meta_params); my $tmpl = $bx->template; ok(! $bx->is_id_only, 'Rule with no filters is not is_id_only'); ok(! $tmpl->is_id_only, 'Rule template with no filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no filters is not is_partial_id'); ok($tmpl->matches_all, 'Rule template with no filters is matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(ui_id => 1, @$meta_params); $tmpl = $bx->template; ok($tmpl->is_id_only, 'Rule with the single ID param is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule with the single ID param is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule with the single ID param is not matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(ui_id => [2], @$meta_params); $tmpl = $bx->template; ok($tmpl->is_id_only, 'Rule with the single ID in-clause param is is_id_only'); ok(! $tmpl->is_partial_id, 'Rule with the single ID in-clause param is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule with the single ID in-clause param is not matches_all'); $bx = URT::UnrelatedItem->define_boolexpr(name => 'foo', @$meta_params); $tmpl = $bx->template; ok(! $tmpl->is_id_only, 'Rule template with no ID filters is not is_id_only'); ok(! $tmpl->is_partial_id, 'Rule template with no ID filters is not is_partial_id'); ok(! $tmpl->matches_all, 'Rule template with no ID filters is not matches_all'); } my @tests = ( # get params property operator expected val [ [ name => 'blah'], 'name', '=', 'blah' ], [ [ name => { operator => '=', value => 'blah'}], 'name', '=', 'blah' ], [ [ 'name =' => 'blah'], 'name', '=', 'blah' ], [ [ name => undef], 'name', '=', undef ], [ [ bar => 1 ], 'bar', '=', 1 ], [ [ bar => { operator => '<', value => 1 }], 'bar', '<', 1 ], [ [ name => [ 'bob', 'joe', 'frank' ] ], 'name', 'in', ['bob','frank','joe']], # list values are sorted [ [ name => { operator => 'not in', value => [1,2,3]} ], 'name', 'not in', [1,2,3] ], [ [ 'name in', => [ 'bob', 'joe', 'frank' ] ], 'name', 'in', ['bob','frank','joe']], [ [ 'name not in' => [ 'bob', 'joe', 'frank' ] ], 'name', 'not in', ['bob','frank','joe']], [ [ name => [ undef ] ], 'name', 'in', [undef] ], [ [ name => { operator => 'in', value => [ undef ] } ], 'name', 'in', [undef] ], [ [ 'name in' => [undef] ], 'name', 'in', [undef] ], [ [ 'name in' => [ 1, undef]], 'name', 'in', [1, undef] ], [ [ bar => { operator => 'between', value => [0,3] } ], 'bar', 'between', [0,3] ], [ [ bar => { operator => 'not between', value => [0,3] } ], 'bar', 'not between', [0,3] ], [ [ 'bar between' => [0,3] ], 'bar', 'between', [0,3] ], [ [ 'bar not between' => [0,3] ], 'bar', 'not between', [0,3] ], [ [ parent => $test_obj ], 'parent_name', '=', 'blah' ], [ [ parent => $test_obj ], 'parent_group','=', 'cool' ], [ [ is_id_only => 1 ], 'is_id_only', '=', 1 ], [ [ a_unique_string => 'hithere'], 'a_unique_string', '=', 'hithere' ], [ [ clobber__get_serial_number => 123], 'clobber__get_serial_number','=', 123], [ [ the_change_count => 456], 'the_change_count', '=', 456], ); for( my $i = 0; $i < @tests; $i++) { my $test = $tests[$i]; my @rule_params = @{ $test->[0] }; my $r = URT::Item->define_boolexpr(@rule_params); ok($r, "Defined a BoolExpr for test $i"); my($property, $expected_operator, $expected_value) = @$test[1..3]; my $got_operator = $r->operator_for($property); is($got_operator, $expected_operator, "Operator for $property is '$expected_operator'"); my $got_value = $r->value_for($property); is_deeply($got_value, $expected_value, "Value for $property matched"); } 49k_complicated_get_joins_with_hangoff_filter.t000444023532023421 600612121654173 24273 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 6; # This test does a query that joins two different tables twice each into a single query # (for a total of 4 tables joined) for a different "reason" each time. # # Loading a row shouldn't cause any additional queries my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, person_id integer not null, key varchar, value varchar)'); # Bob and Fred are people $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into person values (2,'Fred')"); # Bob lives at 123 main st and has a green car $dbh->do("insert into attribute values (11,1,'address','123 main st')"); $dbh->do("insert into attribute values (12,1,'car_color','green')"); # Fred lives at 456 elm st and has a red car $dbh->do("insert into attribute values (21,2,'address','456 elm st')"); $dbh->do("insert into attribute values (22,2,'car_color','red')"); # Bob's father is Fred $dbh->do("insert into attribute values (13,1,'father_id', 2)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, address => { is => 'String', via => 'attributes', to => 'value', where => [key => 'address'] }, father_id => { is => 'Integer', via => 'attributes', to => 'value', where => [key => 'father_id'], is_optional => 1 }, father => { is => 'Person', id_by => 'father_id', is_optional => 1 }, father_address => { via => 'father', to => 'address', is_optional => 1 }, car_color => { is => 'String', via => 'attributes', to => 'value', where => [ key => 'car_color' ] }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my $iter = Person->create_iterator(father_address => '456 elm st'); ok($iter, 'Created iterator for people filter by father_address'); is($query_count, 1, 'Made one query'); $query_count = 0; my $p = $iter->next(); ok($p, 'Got a person'); is($p->name, 'Bob', 'It was the right person'); is($query_count, 0, 'Made no queries'); 63c_view_with_subviews.t.expected.person.text000444023532023421 43412121654173 23645 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tAcme Person 111 name: Fester age: 99 yrs cats: Acme Cat 222 name: fluffy age: 2 yrs fluf: 11 owner: Acme Person 111 (REUSED ADDR) Acme Cat 333 name: nestor age: 8 yrs fluf: 22 yrs owner: Acme Person 111 (REUSED ADDR) 48_inline_datasources.t000444023532023421 2204612121654173 17377 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 43; use File::Temp; &setup_files_and_classes(); foreach my $class_name ( qw( URT::Office URT::Office2 URT::Employee URT::Employee2 URT::Employee3 URT::Employee4 )) { my $class_meta = UR::Object::Type->get($class_name); ok($class_meta, "Loaded class meta for $class_name"); my @ds_name_parts = $class_name =~ m/^(\w+)::(.*)/; my $expected_ds_name = join('::', shift(@ds_name_parts), 'DataSource', @ds_name_parts); is($class_meta->{'data_source_id'}, $expected_ds_name, "It has a data source named"); # my $ds_meta = UR::DataSource->get($class_meta->data_source); # ok($ds_meta, 'Loaded data source meta object'); }; # Try reading from the multi-file data source my $an_office = URT::Office2->get(office_id => 1); ok($an_office, 'Got office with id 1'); is($an_office->address, '123 Main St', 'Address is correct'); foreach my $emp_class ( qw( URT::Employee URT::Employee2 URT::Employee3 URT::Employee4 )) { my $employee = $emp_class->get(division => 'Europe', department => 'RnD', office_address => '345 Fake St'); ok($employee, "Loaded a $emp_class employee by address (delegated property)"); is($employee->emp_id, 5, 'emp_id is correct'); is($employee->name, 'John', 'name is correct'); is($employee->division, 'Europe', 'division is correct'); is($employee->department, 'RnD', 'department is correct'); } my $employee; $employee = eval { URT::Employee->get(); }; ok(!$employee, 'Correctly could not URT::Employee->get() with no params'); like($@, qr/Can't resolve data source: no division specified in rule/, "Error message mentions 'division' property"); my $error_message; UR::DataSource::FileMux->dump_error_messages(0); UR::DataSource::FileMux->error_messages_callback(sub { $DB::single=1; $error_message = $_[1]; }); $employee = eval { URT::Employee->get(division => 'NorthAmerica') }; ok(!$employee, 'Correctly could not URT::Employee->get() with only division'); like($@, qr/Can't resolve data source: no department specified in rule/, "Error message mentions 'department' property"); like($error_message, qr(Recursive entry.*URT::Employee), 'Error message did mention recursive call trapped'); my @employees = eval { URT::Employee->get(division => 'NorthAmerica', department => 'sales') }; ok(! scalar(@employees), 'URT::Employee->get() with non-existent department correctly returns no objects'); is($@, '', 'Correctly, no error message was generated'); @employees = eval { URT::Employee->get(division => 'NorthAmerica', department => 'finance') }; is(scalar(@employees), 3, 'Loaded 3 employees from NorthAmerica/finance'); eval { UR::Object::Type->define( class_name => 'URT::MissingColumnOrder', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { is => 'UR::DataSource::File', file_list => \@office_data_files, }, ); }; ok($@, "missing column_order throws an exception"); sub setup_files_and_classes { our $tmp_dir = File::Temp->newdir('inline_ds_XXXX', TMPDIR => 1, CLEANUP => 1); mkdir $tmp_dir; mkdir "${tmp_dir}/NorthAmerica"; mkdir "${tmp_dir}/Europe"; @office_data_files = ("${tmp_dir}/offices.csv", "${tmp_dir}/offices2.csv"); #our @files_to_remove_later = ( @office_data_files ); # Fill in the data foreach my $name ( @office_data_files ) { my $f = IO::File->new(">$name"); $f->print("1, 123 Main St\n"); $f->print("4, 345 Fake St\n"); $f->print("5, 1 Office Complex Ct\n"); $f->print("100, One Hundred\n"); $f->print("123, 123 Main St\n"); $f->print("350, The Penthouse\n"); $f->close(); } # Yer basic datasource UR::Object::Type->define( class_name => 'URT::Office', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { # This one fills in all the required info is => 'UR::DataSource::File', file => $office_data_files[0], column_order => ['office_id', 'address'], sort_order => ['office_id'], skip_first_line => 0, }, ); # This one discovers columns and sort columns from the class data, and # can read from a list of files UR::Object::Type->define( class_name => 'URT::Office2', id_by => [ office_id => { is => 'Integer' }, ], has => [ address => { is => 'String' }, ], data_source => { is => 'UR::DataSource::File', column_order => ['office_id', 'address'], file_list => \@office_data_files, }, ); unshift @files_to_remove_later, &employee_file_resolver('NorthAmerica','finance'); $f = IO::File->new(">$files_to_remove_later[0]"); $f->print("1\tBob\t100\n"); $f->print("2\tFred\t123\n"); $f->print("3\tJoe\t350\n"); $f->close(); unshift @files_to_remove_later, &employee_file_resolver('Europe', 'RnD'); $f = IO::File->new(">$files_to_remove_later[0]"); $f->print("1\tMike\t1\n"); $f->print("5\tJohn\t4\n"); $f->print("6\tRick\t5\n"); $f->close(); # This one pivots between the two files create above with a function UR::Object::Type->define( class_name => 'URT::Employee', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ 'emp_id', 'name', 'office_id' ], sort_order => [ 'emp_id' ], constant_values => ['division','department'], required_for_get => ['division', 'department'], resolve_path_with => \&employee_file_resolver, }, ); # This one is the same as above, but uses alternate syntax with 'resolve_path_with' UR::Object::Type->define( class_name => 'URT::Employee2', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], resolve_path_with => [\&employee_file_resolver, 'division', 'department'], }, ); # This one uses resolve_path_with with a base_path and list of properties UR::Object::Type->define( class_name => 'URT::Employee3', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], base_path => $tmp_dir, resolve_path_with => ['division','department'], }, ); # This one uses resolve_path_with with an sprintf format UR::Object::Type->define( class_name => 'URT::Employee4', id_by => [ 'emp_id' => { is => 'Integer' }, ], has => [ name => { is => 'String' }, office_id => { is => 'Integer' }, office => { is => 'URT::Office', id_by => 'office_id' }, office_address => { via => 'office', to => 'address' }, division => { is => 'String' }, department => { is => 'String' }, ], data_source => { is => 'UR::DataSource::FileMux', delimiter => "\t", column_order => [ qw( emp_id name office_id ) ], sort_order => [ 'emp_id' ], resolve_path_with => ["${tmp_dir}/%s/%s", 'division','department'], }, ); } sub employee_file_resolver { my($division, $department) = @_; our $tmp_dir; sprintf("${tmp_dir}/$division/$department"); } 07_create_get_simple.t000444023532023421 264212121654173 17152 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my ($p1,$p2,$p3,$p4,$p5,$p6,$p7,@obj,@got,@expected); use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name/] ); $p1 = Acme::Product->create(name => "jet pack", manufacturer_name => "Lockheed Martin"); ok($p1, 'Created a jet pack'); $p2 = Acme::Product->create(name => "hang glider", manufacturer_name => "Boeing"); ok($p2, 'Created a hang glider'); $p3 = Acme::Product->create(name => "mini copter", manufacturer_name => "Boeing"); ok($p2, 'Created a mini copter'); $p4 = Acme::Product->create(name => "firecracker", manufacturer_name => "Explosives R US"); ok($p2, 'Created a firecracker'); $p5 = Acme::Product->create(name => "dynamite", manufacturer_name => "Explosives R US"); ok($p2, 'Created a dynamite'); $p6 = Acme::Product->create(name => "plastique", manufacturer_name => "Explosives R US"); ok($p2, 'Created a plastique'); @obj = Acme::Product->get(manufacturer_name => "Boeing"); is(scalar(@obj), 2, 'Two objects have manufacturer_name => "Boeing"'); # @obj = Acme::Product->get(); is(scalar(@obj), 6, 'There were six objects total'); @got = sort @obj; @expected = sort ($p1,$p2,$p3,$p4,$p5,$p6); is_deeply(\@got,\@expected, 'They are in the expected order'); 56_order_by_returns_items_in_order.t000444023532023421 2566412121654173 22205 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 31; # When a different ordering is requested, make sure a get() that hits # the DB returns items in the same order as one that returns cached objects. # It should be sorted first by the requested key, then by ID &setup_classes_and_db(); my @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); my @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; my @expected = ( { id => 5, name => 'Bobs', data => 'aaa' }, { id => 2, name => 'Bob', data => 'abc' }, { id => 4, name => 'Bobby', data => 'abc' }, { id => 6, name => 'Bobert', data => 'infinity' }, { id => 1, name => 'Bobert', data => 'zzz' }, { id => 0, name => 'Bobbb', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Bob% ordered by data'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Bob%', -order => ['data']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Bob% ordered by data from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Now do descending @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Try order by -id $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-id']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 16, name => 'Fredert', data => 'infinity' }, { id => 15, name => 'Freds', data => 'aaa' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by id DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['-id']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by id DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Now, try multiple order bys $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 15, name => 'Freds', data => 'aaa' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data,name from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # multiple, different order bys $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','-name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 15, name => 'Freds', data => 'aaa' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 10, name => 'Freddd', data => undef }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['data','-name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data, name DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # different order bys in the other order $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # And now both descending $_->unload foreach @o; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','-name']); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; @expected = ( { id => 10, name => 'Freddd', data => undef }, { id => 11, name => 'Fredert', data => 'zzz' }, { id => 16, name => 'Fredert', data => 'infinity' }, { id => 14, name => 'Freddy', data => 'abc' }, { id => 12, name => 'Fred', data => 'abc' }, { id => 15, name => 'Freds', data => 'aaa' }, ); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name DESC'); is_deeply(\@got, \@expected, 'Returned data is as expected') or diag(Data::Dumper::Dumper(@got)); # Now try it again, cached $DB::single=1; @o = URT::Thing->get('name like' => 'Fred%', -order => ['-data','-name']); is(scalar(@o), scalar(@expected), 'Got correct number of things with name like Fred% ordered by data DESC, name DESC from the cache'); @got = map { { id => $_->id, name => $_->name, data => $_->data } } @o; is_deeply(\@got, \@expected, 'Returned cached data is as expected') or diag(Data::Dumper::Dumper(\@got,\@expected)); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); # Inserting them purposfully in non-ID order so they'll get returned in non-id # order if the ID column isn't included in the 'order by' clause foreach my $row ( ( [4, 'Bobby', 'abc'], [2, 'Bob', 'abc'], [0, 'Bobbb', undef], [1, 'Bobert', 'zzz'], [6, 'Bobert', 'infinity'], [5, 'Bobs', 'aaa'], [14, 'Freddy', 'abc'], [12, 'Fred', 'abc'], [10, 'Freddd', undef], [11, 'Fredert', 'zzz'], [16, 'Fredert', 'infinity'], [15, 'Freds', 'aaa'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ 'name' => { is => 'String' }, 'data' => { is => 'String', is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); } 95c_detect_changed_in_memory_filter.t000444023532023421 252112121654173 22211 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar, other varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?,?)'); foreach my $id ( 2..10 ) { $sth->execute($id, chr($id + 64), chr($id + 64)) } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value','other'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # Changing an object in memory and filtering on that change should not register as a DB deletion. # - Had to get with multiple IDs in order to reproduce the bug we found. # - Had to separate this in its own test because 95_detect_db_deleted.t's environment was unable to reproduce the bug. # - The bug was that UR::Context::LoadingIterator was treating the case where a BoolExpr filter change was causing an exception to be thrown. my @ids = (3, 5, 7); my @things = URT::Thing->get(id => \@ids); map { $_->value('A') } @things; my @same_things = URT::Thing->get(value => 'A', id => \@ids); is(scalar @things, scalar @same_things, 'got same number of same things as we created A'); done_testing(); 29c_join_indirect_accessor.t000444023532023421 756512121654173 20361 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 10; # Tests a get() with an indirect property, where the delegation is resolved via # another delegated property ok(setup(), 'Create initial schema, data and classes'); my $emp = URT::Employee->get(1); ok($emp, 'Got employee 1'); my $boss = $emp->boss; is($boss->first_name, 'Bob', 'Got boss for employee 1'); my $company = $emp->company(); is($company->name, 'CoolCo', 'Got company for employee 1'); # For now, this is pretty inefficient. An Employee's company_id is delegated through boss, # which results in a tree structure for its join requirements. my @emp = URT::Employee->get(company_name => 'CoolCo'); is(scalar(@emp), 2, 'Got 2 employees of CoolCo'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do('create table COMPANY (company_id, name varchar)'), 'create table COMPANY'); ok($dbh->do('create table BOSS (boss_id int, first_name varchar, last_name varchar, company_id int REFERENCES company(company_id))'), 'create table BOSS'); ok($dbh->do('create table EMPLOYEE (emp_id int, name varchar, is_secret, int, boss_id int references BOSS(BOSS_ID))'), 'create table EMPLOYEE'); my $sth = $dbh->prepare('insert into COMPANY (company_id, name) values (?,?)'); $sth->execute(1, 'CoolCo'); $sth->execute(2, 'Data Inc'); $sth->finish; $sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company_id) values (?,?,?,?)'); $sth->execute(1, 'Bob', 'Smith', 1); $sth->execute(2, 'Robert', 'Jones', 2); $sth->finish(); $sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)'); $sth->execute(1,'Joe', 1, 0); $sth->execute(2,'James', 1, 0); $sth->execute(3,'Jack', 2, 1); $sth->execute(4,'Jim', 2, 0); $sth->execute(5,'Jacob', 2, 1); $sth->finish(); ok($dbh->commit(), 'Commit records to DB'); UR::Object::Type->define( class_name => 'URT::Company', id_by => 'company_id', has => ['name'], table_name => 'COMPANY', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, company_id => { type => "Number" }, company => { is => 'URT::Company', id_by => 'company_id' }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); # An employee's boss is connected through the calculated property calc_boss_id UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, boss => { type => "URT::Boss", id_by => 'boss_id' }, company_id => { via => 'boss' }, company => { is => 'URT::Company', id_by => 'company_id' }, company_name => { via => 'company', to => 'name' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } 63d_delete_view.t000444023532023421 351012121654173 16134 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; if ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'}) { plan tests => 8; } else { plan skip_all => 'works only with systems which have XML::LibXML and XML::LibXSLT'; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, ] }; my $p = Person->create(name => 'Fester', age => 99); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); my $pv = $p->create_view( toolkit => 'xml', aspects => [ 'name', 'age', { name => 'cats', perspective => 'default', toolkit => 'xml', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ] ); ok($pv, "got an xml view for the person"); my $pv_got_content = $pv->content; my $c1v = $c1->create_view(toolkit => 'xml'); ok($c1v, 'Created xml view for a cat'); ok($c1v, "got a xml view for one of the cats"); my $c1v_got_content = $c1v->content; ok($c1v_got_content, 'Cat xml view generated some content'); UR::Context->current->rollback; 27_get_with_limit_offset.t000444023532023421 564112121654173 20061 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 38; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table thing ( thing_id integer not null primary key, name varchar )'), 'created node table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); foreach my $i ( 1 .. 100 ) { $sth->execute($i,$i); } ok($sth->finish, 'Insert test data into DB'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Integer' }, ], has => [ name => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); for my $try (1 .. 2) { my @o = URT::Thing->get(-limit => 5); is(scalar(@o), 5, 'Got 5 things with limit'); my $o = get_ids(@o); is_deeply($o, [1..5],'Got the right objects back'); @o = URT::Thing->get('thing_id >' => 10, -limit => 5); is(scalar(@o), 5, 'Got 5 things with filter and limit'); $o = get_ids(@o); is_deeply($o, [11..15], 'Got the right objects back'); @o = URT::Thing->get('thing_id <' => 50, -limit => 2, -offset => 10); is(scalar(@o), 2, 'Got two objects with -limit 2 and -offset 10'); $o = get_ids(@o); is_deeply($o, [11,12], 'Got the right objects back'); @o = URT::Thing->get('thing_id <' => 70, -page => [6,3]); is(scalar(@o), 3, 'Got 3 things with -page [6,3]'); $o = get_ids(@o); is_deeply($o, [16,17,18], 'Got the right objects back'); my $iter = URT::Thing->create_iterator('thing_id >' => 30, -limit => 5); ok($iter, 'Created iterator with -limit'); @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 5, 'Got 5 things with iterator'); $o = get_ids(@o); is_deeply($o, [31 .. 35], 'Got the right objects back'); $iter = URT::Thing->create_iterator('thing_id >' => 35, -limit => 3, -offset => 15); ok($iter, 'Created iterator with -limit and -offset'); @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 3, 'Got 3 things with iterator'); $o = get_ids(@o); is_deeply($o, [51,52,53], 'Got the right objects back'); if ($try == 1) { @o = URT::Thing->get(); # To get everything into the cache ok(scalar(@o), 'Get all objects into cache and try the tests again'); } $iter = URT::Thing->create_iterator('thing_id >' => 70, -page => [5,2]); ok($iter, 'Create iterator with -page [5,2]'); @o = (); while(my $o = $iter->next()) { push @o, $o; } is(scalar(@o), 2,'Got 2 things with iterator'); $o = get_ids(@o); is_deeply($o, [79,80], 'Got the right objects back'); } sub get_ids { my @list = map { $_->id} @_; return \@list; } 62_in_not_in_operator.t000444023532023421 1612612121654173 17411 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 44; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my(@things,%things_by_id); @things = URT::Thing->get(value => [1,2,3]); is(scalar(@things), 3, 'Got 3 things from the DB with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'1'}->id, 1, 'Got value 1'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,4,5]); is(scalar(@things), 3, 'Got 3 things from the DB with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'6'}->id, 6, 'Got value 6'); is($things_by_id{'7'}->id, 7, 'Got value 7'); is($things_by_id{'8'}->id, 8, 'Got value 8'); @things = URT::Thing->get(value => [1,2,3]); is(scalar(@things), 3, 'Got 3 things from the cache with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'1'}->id, 1, 'Got value 1'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,4,5]); is(scalar(@things), 3, 'Got 3 things from the cache with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'6'}->id, 6, 'Got value 6'); is($things_by_id{'7'}->id, 7, 'Got value 7'); is($things_by_id{'8'}->id, 8, 'Got value 8'); @things = URT::Thing->get(value => [ 2,3,4 ]); is(scalar(@things), 3, 'Got 3 things from the DB and cache with IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'4'}->id, 4, 'Got value 4'); is($things_by_id{'2'}->id, 2, 'Got value 2'); is($things_by_id{'3'}->id, 3, 'Got value 3'); @things = URT::Thing->get('value not in' => [1,2,3,7,8]); is(scalar(@things), 3, 'Got 3 things from the DB and cache with NOT IN'); %things_by_id = map { $_->value => $_ } @things; is($things_by_id{'4'}->id, 4, 'Got value 4'); is($things_by_id{'5'}->id, 5, 'Got value 5'); is($things_by_id{'6'}->id, 6, 'Got value 6'); @things = URT::Thing->get('related_values in' => [1,2,3]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values IN 1-3'); @things = URT::Thing->get('related_values in' => [-1,-2,9,10]); is(scalar(@things), 0, 'Got 0 things with related_values in [-1,-2,9,10]'); # All of them will match value 6 @things = URT::Thing->get('related_values in' => [-1, -2, 6]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values IN [-1, -2, 6]'); @things = URT::Thing->get('related_values not in' => [-10,-9,9,99]); is(scalar(@things), 8, 'Got 8 things from the DB with related_values not in [-10,-9,9,99]'); @things = URT::Thing->get('related_values not in' => [4,5]); is(scalar(@things), 8, 'Got 0 things with related_values not in [4,5]'); # all of them have value 7 @things = URT::Thing->get('related_values not in' => [7,100,101]); is(scalar(@things), 8, 'Got 0 things with related_values not in [7,100,101]'); @things = URT::Thing->get('related_values not in' => [1,2,3,4,5,6,7,8]); is(scalar(@things), 0, 'Got 0 things with related_values not in [1,2,3,4,5,6,7,8]'); # Only things 1 and 2 have optional values set @things = URT::Thing->get('related_optional_values in' => [1,2,3]); is(scalar(@things), 2, 'Got 2 things from DB with related_optional_values in 1-3'); @things = URT::Thing->get('related_optional_values in' => [20,4,16]); is(scalar(@things), 2, 'Got 2 things with related_optional_values in [4,16,20]'); @things = URT::Thing->get('related_optional_values in' => [25,26,-2]); is(scalar(@things), 0, 'Got 0 things with related_optional_values in [-2,25,26]'); @things = URT::Thing->get('related_optional_values in' => [19, undef, 5]); is(scalar(@things), 8, 'All 8 things with related_optional_values in [undef, 5,19]'); # objs 1 and 2 will match the "related values is not null" part @things = URT::Thing->get('related_optional_values not in' => [undef, 6, 22]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [undef, 6, 22]'); # 1 and 2 have related values not in 7,8 (1-6, for example). The others (objs 3-8) are NULL and don't match @things = URT::Thing->get('related_optional_values not in' => [7,8]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [7,8]'); # Same here, 1 and 2 have related values not in the list. Others are NULL @things = URT::Thing->get('related_optional_values not in' => [500,501, -22]); is(scalar(@things), 2, 'Got 2 things with related_optional_values not in [500,501, -22]'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value integer)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($val,$val); } $sth->finish; ok( $dbh->do("create table related (related_id integer NOT NULL PRIMARY KEY, thing_id integer references thing(thing_id), value integer)"), 'created related table'); $sth = $dbh->prepare('insert into related values (?,?,?)'); my $id = 1; foreach my $val ( 1,2,3,4,5,6,7,8 ) { foreach my $thing_id ( 1..8 ) { $sth->execute($id++,$thing_id,$val); } } $sth->finish; ok( $dbh->do("create table related_optional (related_id integer NOT NULL PRIMARY KEY, thing_id integer references thing(thing_id), value integer)"), 'created related_optional table'); $sth = $dbh->prepare('insert into related_optional values (?,?,?)'); $id = 1; foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($id++,1,$val); $sth->execute($id++,2,$val); } $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'Integer' }, ], has_many => [ relateds => { is => 'URT::Related', reverse_as => 'thing' }, related_values => { via => 'relateds', to => 'value' }, ], has_many_optional => [ related_optionals => { is => 'URT::RelatedOptional', reverse_as => 'thing' }, related_optional_values => { via => 'related_optionals', to => 'value' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Related', id_by => 'related_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related', ); UR::Object::Type->define( class_name => 'URT::RelatedOptional', id_by => 'related_id', has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, value => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'related_optional', ); } 69_subclassify_by.t000444023532023421 3353312121654173 16553 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 102; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); note('Tests for subclassing by regular property'); our $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Employee', subclassify_by => 'subclass_name', is_abstract => 1, has => [ name => { type => "String" }, subclass_name => { type => 'String' }, ], ); UR::Object::Type->define( class_name => 'Acme::Employee::Worker', is => 'Acme::Employee', ); UR::Object::Type->define( class_name => 'Acme::Employee::Boss', is => 'Acme::Employee', ); my $e1 = eval { Acme::Employee->create(name => 'Bob') }; ok(! $e1, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Can't use undefined value as a subclass name/, 'The exception was correct'); $e1 = Acme::Employee->create(name => 'Bob', subclass_name => 'Acme::Employee::Worker'); ok($e1, 'Created an object from the base class and specified subclass_name'); isa_ok($e1, 'Acme::Employee::Worker'); is($e1->name, 'Bob', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Worker', 'subclass_name is correct'); $e1 = Acme::Employee::Worker->create(name => 'Bob2'); ok($e1, 'Created an object from a subclass without subclass_name'); isa_ok($e1, 'Acme::Employee::Worker'); is($e1->name, 'Bob2', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Worker', 'subclass_name is correct'); $e1 = Acme::Employee->create(name => 'Fred', subclass_name => 'Acme::Employee::Boss'); ok($e1, 'Created an object from the base class and specified subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = Acme::Employee::Boss->create(name => 'Fred2'); ok($e1, 'Created an object from a subclass without subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred2', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = Acme::Employee::Boss->create(name => 'Fred3', subclass_name => 'Acme::Employee::Boss'); ok($e1, 'Created an object from a subclass and specified the same subclass_name'); isa_ok($e1, 'Acme::Employee::Boss'); is($e1->name, 'Fred3', 'Name is correct'); is($e1->subclass_name, 'Acme::Employee::Boss', 'subclass_name is correct'); $e1 = eval { Acme::Employee::Worker->create(name => 'Joe', subclass_name => 'Acme::Employee') }; ok(! $e1, 'Creating an object from a subclass with the base class as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee\) does not match the class it was called on \(Acme::Employee::Worker\)/, 'Exception was correct'); $e1 = eval { Acme::Employee::Worker->create(name => 'Joe', subclass_name => 'Acme::Employee::Boss') }; ok(! $e1, 'Creating an object from a subclass with another subclass as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee::Boss\) does not match the class it was called on \(Acme::Employee::Worker\)/, 'Exception was correct'); $e1 = eval { Acme::Employee::Boss->create(name => 'Joe', subclass_name => 'Acme::Employee::Worker') }; ok(! $e1, 'Creating an object from a subclass with another subclass as subclass_name did not work'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Employee::Worker\) does not match the class it was called on \(Acme::Employee::Boss\)/, 'Exception was correct'); $e1 = eval { Acme::Employee->create(name => 'Mike', subclass_name => 'Acme::Employee::NonExistent') }; ok(! $e1, 'Creating an object from the base class and gave invalid subclass_name did not work'); like($@, qr/Class Acme::Employee::NonExistent is not a subclass of Acme::Employee/, 'Exception was correct'); note('Tests for default value subclassing'); UR::Object::Type->define( class_name => 'Acme::Tool', is_abstract => 1, subclassify_by => 'subclass_name', has => [ sku => { is => 'Number'}, subclass_name => { is => 'String', default_value => 'Acme::Tool::Generic' }, ], ); UR::Object::Type->define( class_name => 'Acme::Tool::Hammer', is => 'Acme::Tool', ); UR::Object::Type->define( class_name => 'Acme::Tool::Generic', is => 'Acme::Tool', ); my $t = eval { Acme::Tool->create(sku => 123) }; ok($t, 'Created an Acme::Tool without subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name took the default value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool->create(sku => 234, subclass_name => 'Acme::Tool::Generic') }; ok($t, 'Created an Acme::Tool with subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 456) }; ok($t, 'Created an Acme::Tool::Generic without subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 456, subclass_name => 'Acme::Tool::Generic') }; ok($t, 'Created an Acme::Tool::Generic with subclass_name'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Generic', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Generic'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Generic->create(sku => 567, subclass_name => 'Acme::Tool::Broken') }; ok(! $t, 'Did not create an Acme::Tool::Generic with a non-matching subclass_name'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Tool::Broken\) does not match the class it was called on \(Acme::Tool::Generic\)/, 'Exception was correct'); $t = eval { Acme::Tool->create(sku => 678, subclass_name => 'Acme::Tool::Hammer') }; ok($t, 'Created an Acme::Tool with subclass_name Acme::Tool::Hammer'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Hammer', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Hammer'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Hammer->create(sku => 789, subclass_name => 'Acme::Tool::Hammer') }; ok($t, 'Created an Acme::Tool::Hammer with subclass_name Acme::Tool::Hammer'); ok(! $@, 'No exception during create'); is($t->subclass_name, 'Acme::Tool::Hammer', 'subclass_name has the correct value'); isa_ok($t, 'Acme::Tool::Hammer'); isa_ok($t, 'Acme::Tool'); $t = eval { Acme::Tool::Hammer->create(sku => 678, subclass_name => 'Acme::Tool::Generic') }; ok(! $t, 'Did not create an Acme::Tool::Hammer with a non-matching subclass_name'); like($@, qr/Value for subclassifying param 'subclass_name' \(Acme::Tool::Generic\) does not match the class it was called on \(Acme::Tool::Hammer\)/, 'Exception was correct'); note('Tests for indirect property subclassing'); UR::Object::Type->define( class_name => 'Acme::Rank', has => [ name => { is => 'String' }, soldier_subclass => { is_calculated => 1, calculate => q( return 'Acme::Soldier::'.ucfirst($self->name) ) }, ] ); UR::Object::Type->define( class_name => 'Acme::Soldier', is_abstract => 1, subclassify_by => 'subclass_name', has => [ name => { is => 'String' }, rank => { is => 'Acme::Rank', id_by => 'rank_id' }, subclass_name => { via => 'rank', to => 'soldier_subclass' }, ], ); UR::Object::Type->define( class_name => 'Acme::Soldier::Private', is => 'Acme::Soldier', ); UR::Object::Type->define( class_name => 'Acme::Soldier::General', is => 'Acme::Soldier', ); my $private = Acme::Rank->create(name => 'Private'); my $general = Acme::Rank->create(name => 'General'); is($private->soldier_subclass, 'Acme::Soldier::Private', 'Private Rank returns correct soldier subclass'); is($general->soldier_subclass, 'Acme::Soldier::General', 'General Rank returns correct soldier subclass'); my $s = eval { Acme::Soldier->create(name => 'Pyle') }; ok(!$s, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Infering a value for property 'subclass_name' via rule.*returned multiple values/, 'Exception is correct'); $s = eval { Acme::Soldier->create(name => 'Pyle', rank => $private) }; ok($s, 'Created object from abstract parent, subclassed via an indirect object property'); is($s->subclass_name, 'Acme::Soldier::Private', 'subclass_name is correct'); isa_ok($s, 'Acme::Soldier::Private'); $s = eval { Acme::Soldier->create(name => 'Pyle', rank_id => $private->id) }; ok($s, 'Created object from abstract parent, subclassed via an indirect object ID'); is($s->subclass_name, 'Acme::Soldier::Private', 'subclass_name is correct'); isa_ok($s, 'Acme::Soldier::Private'); $s = Acme::Soldier->create(name => 'Pyle', subclass_name => 'Acme::Soldier::Private'); ok($s, 'Created object from abstract parent with subclass_name'); isa_ok($s, 'Acme::Soldier::Private'); is($s->rank, $private, 'Rank object was filled in properly'); $s = Acme::Soldier::Private->create(name => 'Beetle'); ok($s, 'Created object from child class'); isa_ok($s, 'Acme::Soldier::Private'); is($s->rank_id, $private->id, 'Its rank_id points to the Private Rank object'); $s = eval { Acme::Soldier::Private->create(name => 'Patton', rank => $general) }; ok(! $s, 'Unable to create an object from a child class when its rank indicates a different subclass'); like($@, qr/Conflicting values for property 'rank_id'/, 'Exception is correct'); note('Tests for calculated subclassing'); # First, setup a table we'll use in the next section of tests... my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do(q(create table vehicle (vehicle_id integer NOT NULL PRIMARY KEY, color varchar NOT NULL, wheels integer NOT NULL))); $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Vehicle', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'vehicle_id', has => [ color => { is => 'String' }, wheels => { is => 'Integer' }, subclass_name => { calculate_from => ['wheels'], calculate => sub { my $wheels = shift; $calculate_called = 1; no warnings 'uninitialized'; if (! defined $wheels) { return; } elsif ($wheels == 2) { return 'Acme::Motorcycle'; } elsif ($wheels == 4) { return 'Acme::Car'; } elsif ($wheels == 0) { return 'Acme::Sled'; } else { die "Can't create a vehicle with $wheels wheels"; } }, }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'vehicle', ); UR::Object::Type->define( class_name => 'Acme::Motorcycle', is => 'Acme::Vehicle', ); UR::Object::Type->define( class_name => 'Acme::Car', is => 'Acme::Vehicle', ); UR::Object::Type->define( class_name => 'Acme::Sled', is => 'Acme::Vehicle', ); $calculate_called = 0; my $v = eval { Acme::Vehicle->create(color => 'blue') }; ok(! $v, 'Unable to create an object from the abstract class without a subclass_name'); like($@, qr/Class Acme::Vehicle subclassify_by calculation property 'subclass_name' requires 'wheels' in the create\(\) params/, 'Exception was correct'); ok(! $calculate_called, 'The calculation function was called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'blue', wheels => 2, subclass_name => 'Acme::Motorcycle'); ok($v, 'Created an object from the base class by specifying subclass_name'); isa_ok($v, 'Acme::Motorcycle'); ok(! $calculate_called, 'The calculation function was not called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'green', wheels => 3, subclass_name => 'Acme::Motorcycle'); ok($v, 'Created another object from the base class'); isa_ok($v, 'Acme::Motorcycle'); ok(! $calculate_called, 'The calculation function was not called'); $calculate_called = 0; $v = Acme::Vehicle->create(color => 'red', wheels => 4); ok($v, 'Created an object from the base class by specifying wheels'); isa_ok($v, 'Acme::Car'); ok($calculate_called, 'The calculation function was called'); $calculate_called = 0; is($v->subclass_name, 'Acme::Car', "It's subclass_name property is filled in"); ok(! $calculate_called, "Reading the subclass_name property didn't call the calculation sub"); note('Tests for loading with calculated subclassing'); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (99, 'blue', 2))); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (98, 'green', 3))); $dbh->do(q(insert into vehicle(vehicle_id, color, wheels) values (97, 'red', 4))); $calculate_called = 0; $v = Acme::Vehicle->get(99); ok($v, 'Get an Acme::Vehicle out of the DB'); ok($calculate_called, 'The calculation function was called'); isa_ok($v, 'Acme::Motorcycle'); $calculate_called = 0; $v = eval { Acme::Vehicle->get(98) }; ok(! $v, 'Acme::Vehicle with 3 wheels failed to load'); ok($calculate_called, 'The calculation function was called'); like($@, qr/Can't create a vehicle with 3 wheels/, 'Exception was correct'); 03f_rule_from_filter_string.t000444023532023421 6534712121654173 20621 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl # Test the Parse::YAPP parser used by the Lister commands use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 710; class URT::RelatedItem { id_by => 'ritem_id', has => [ ritem_property => { is => 'String' }, ritem_number => { is => 'Number' }, ], }; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, score => { is => 'Integer' }, ritem => { is => 'URT::RelatedItem', id_by => 'ritem_id' }, desc => { is => 'String' }, ] }; foreach my $test ( { string => 'name = bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name=bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name=>bob', values => { name => 'bob' }, operators => { name => '=' }, }, { string => 'name=a-longer-string', values => { name => 'a-longer-string' }, operators => { name => '=' }, }, { string => 'name=2012-jan-12', values => { name => '2012-jan-12' }, operators => { name => '=' }, #stop => 1, }, { string => 'name=some.thing', values => { name => 'some.thing' }, operators => { name => '='}, }, { string => 'name=/some/file.path.ext', values => { name => '/some/file.path.ext' }, operators => { name => '='}, }, { string => 'name=Some::Class::Name', values => { name => 'Some::Class::Name' }, operators => { name => '='}, }, { string => 'name:Some::Class/Other::Class/Third::Class,score =2', values => { name => ['Other::Class','Some::Class','Third::Class'], score => 2 }, operators => { name => 'in', score => '='}, }, { string => 'name in [Some::Class, Other::Class, Third::Class] and score = 2', values => { name => ['Other::Class','Some::Class','Third::Class'], score => 2 }, operators => { name => 'in', score => '='}, }, { string => 'name=fred and score>2', values => { name => 'fred', score => 2 }, operators => { name => '=', score => '>'}, }, { string => 'name=",",score=2', values => { name => ',', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name=and and score=2' , values => { name => 'and', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name in [bob,fred] and score<-2', values => { name => ['bob','fred'], score => -2 }, operators => { name => 'in', score => '<' } }, { string => 'score = -12.2' , values => { score => -12.2 }, operators => { score => '=' }, }, { string => 'score = .2' , values => { score => .2 }, operators => { score => '=' }, }, { string => 'score = -.2' , values => { score => -0.2 }, operators => { score => '=' }, }, { string => 'name=fred and score>2,foo=bar', values => { name => 'fred', score => 2, foo => 'bar' }, operators => { name => '=', score => '>', foo => '='} }, { string => 'name=fred and score>=2', operators => { name => '=', score => '>=' }, values => { name => 'fred', score => 2}, }, { string => 'name=fred and score<=2', operators => { name => '=', score => '<=' }, values => { name => 'fred', score => 2}, }, { string => 'score!:-100--10.2', values => { score => [-100, -10.2] }, operators => { score => 'not between' }, #stop => 1, }, { string => 'name~%yoyo,score:10-100', values => { name => '%yoyo', score => [10,100] }, operators => { name => 'like', score => 'between' } }, { string => 'name like yoyo', values => { name => '%yoyo%' }, operators => { name => 'like' } }, { string => 'name like something-with-dashes1795%', values => { name => 'something-with-dashes1795%' }, operators => { name => 'like' }, }, { string => 'name like H_%-MPaS3387-1795-lib2', values => { name => 'H_%-MPaS3387-1795-lib2' }, operators => { name => 'like' }, }, { string => 'name like %some/file/path-name.ext', values => { name => '%some/file/path-name.ext' }, operators => { name => 'like' }, }, { string => 'name like 1234% and desc not like %bar%', values => { name => '1234%', desc => '%bar%' }, operators => { name => 'like', desc => 'not like' }, }, { string => 'foo:one/two/three', values => { foo => ['one','three','two'] }, # They get sorted internally operators => { foo => 'in' }, }, { string => 'foo!:one/two/three', values => { foo => ['one','three','two'] }, # They get sorted internally operators => { foo => 'not in' }, }, { string => 'name=/a/path/name', values => { name => '/a/path/name' }, operators => { name => '=' }, }, { string => 'name:a/path/name', values => { name => ['a','name','path'] }, operators => { name => 'in' }, }, { string => 'name in ["/a/path/name","/other/path/","relative/path/name"]', values => { name => ['/a/path/name','/other/path/','relative/path/name'] }, operators => {name => 'in' }, }, { string => 'score in [1,2,3]', values => { score => [1,2,3] }, operators => { score => 'in' }, }, { string => 'score not in [1,2,3]', values => { score => [1,2,3] }, operators => { score => 'not in' }, }, { string => 'foo:one/two/three,score:10-100', # These both use : values => { foo => ['one','three','two'], score => [10,100] }, operators => { foo => 'in', score => 'between' }, }, { string => 'foo!:one/two/three,score:10-100', # These both use : values => { foo => ['one','three','two'], score => [10,100] }, operators => { foo => 'not in', score => 'between' }, }, { string => q(name="bob is cool",foo:'one "two"'/three), values => { name => 'bob is cool', foo => ['one "two"','three'] }, operators => { name => '=', foo => 'in' }, }, { string => 'name not like %joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name ! like %joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name !~%joe', values => { name => '%joe' }, operators => { name => 'not like' }, }, { string => 'name not like %joe and score!:10-100 and foo!:one/two/three', values => { name => '%joe', score => [10,100], foo => ['one', 'three', 'two'] }, operators => { name => 'not like', score => 'not between', foo => 'not in' } }, { string => 'name=foo and ritem.ritem_property=bar', values => { name => 'foo', 'ritem.ritem_property' => 'bar' }, operators => { name => '=', 'ritem.ritem_property' => '=' }, }, { string => 'name=foo,ritem.ritem_property=bar,ritem.ritem_number=.2', values => { name => 'foo', 'ritem.ritem_property' => 'bar','ritem.ritem_number' => 0.2 }, operators => { name => '=', 'ritem.ritem_property' => '=', 'ritem.ritem_number' => '=' }, }, { string => 'name=foo and foo=bar and score=2', values => { name => 'foo', foo => 'bar', score => 2 }, operators => { name => '=', foo => '=', score => '=' }, }, { string => 'name=foo and ( foo=bar and score=2 )', values => { name => 'foo', foo => 'bar', score => 2 }, operators => { name => '=', foo => '=', score => '=' }, }, { string => 'name=foo limit 10', values => { name => 'foo' }, operators => {name => '='}, limit => 10, }, { string => 'name=foo offset 10', values => { name => 'foo' }, operators => {name => '='}, offset => 10, }, { string => 'name=foo limit 10 offset 20', values => { name => 'foo' }, operators => {name => '='}, limit => 10, offset => 20, }, { string => 'name=foo and score=2 limit 10 offset 20', values => { name => 'foo', score => 2 }, operators => {name => '=', score => '='}, limit => 10, offset => 20, }, { string => 'name=foo order by score' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score'], }, { string => 'name=foo order by score asc' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score'], }, { string => 'name=foo order by -score' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score'], }, { string => 'name=foo order by score desc' , values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score'], }, { string => 'name=foo order by score,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score asc,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score asc,foo asc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','foo'], }, { string => 'name=foo order by score,-foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','-foo'], }, { string => 'name=foo order by score,foo desc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['score','-foo'], }, { string => 'name=foo order by -score,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by score desc,foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by score desc,foo asc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','foo'], }, { string => 'name=foo order by -score,-foo', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], }, { string => 'name=foo order by score desc,foo desc', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], }, { string => 'name=foo order by -score,-foo group by ritem_id', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id'], }, { string => 'name=foo order by score desc,foo desc group by ritem_id', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id'], }, { string => 'name=foo order by -score,-foo group by ritem_id, parent_name', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id','parent_name'], }, { string => 'name=foo order by -score,-foo group by ritem_id, parent_name limit 10 offset 20', values => { name => 'foo' }, operators => { name => '=' }, order_by => ['-score','-foo'], group_by => ['ritem_id','parent_name'], limit => 10, offset => 20, }, { string => '', values => {}, operators => {}, }, { string => 'order by score', values => {}, operators => {}, order_by => ['score'], }, { string => 'name = a string and score=2', values => { name => 'a string', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name=a string with some more words and score = 2', values => { name => 'a string with some more words', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name=a string with spaces in between the words and score =2', values => { name => 'a string with spaces in between the words', score => 2 }, operators => { name => '=', score => '=' }, }, { string => 'name=a string with multiple spaces and score = 2', values => { name => 'a string with multiple spaces', score => 2}, operators => { name => '=', score => '=' }, }, { string => 'name true', operators => { name => 'true' }, values => { name => 1 }, }, { string => 'name false', operators => { name => 'false' }, values => { name => 1 }, }, { string => 'name true and score=2', operators => { name => 'true', score => '=' }, values => { name => 1, score => 2 }, }, { string => 'name is null', operators => { name => '=' }, values => { name => undef }, }, { string => 'name is not null', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name is undef', operators => { name => '=' }, values => { name => undef }, }, { string => 'name is not undef', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name not is undef', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name not is null', operators => { name => '!=' }, values => { name => undef }, }, { string => 'name is not undef and score=2', operators => { name => '!=', score => '=' }, values => { name => undef, score => 2 }, }, { string => 'name=this that + the other thing', operators => { name => '=' }, values => { name => 'this that + the other thing' }, }, ) { my $string = $test->{'string'}; my $values = $test->{'values'}; my $value_count = scalar(values %$values); my @properties = keys %$values; my $operators = $test->{'operators'}; my $r = UR::BoolExpr->resolve_for_string( 'URT::Item', $test->{'string'}); ok($r, "Created rule from string \"$string\""); my @got_values = $r->values(); is(scalar(@got_values), $value_count, 'Rule has the right number of values'); foreach my $property (@properties) { is_deeply($r->value_for($property), $values->{$property}, "Value for $property is correct"); is($r->operator_for($property), $operators->{$property}, "Operator for $property is correct"); } foreach my $meta ( 'order_by', 'group_by', 'limit', 'offset' ) { if ($test->{$meta}) { my $got = $r->template->$meta; is_deeply($got, $test->{$meta}, "$meta is correct"); } } exit if ($test->{'stop'}); # print Data::Dumper::Dumper($r); } #exit; # or-type rules need to be checked differently foreach my $test ( { string => 'name=bob or foo=bar', rules => [ { values => { name => 'bob' }, operators => { name => '=' }, }, { values => { foo => 'bar' }, operators => { foo => '=' }, } ], }, { string => 'name=bob and score=2 or name =fred and foo=bar', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'fred', foo => 'bar' }, operators => { name => '=', foo => '=' }, } ], }, { string => 'name=bob or name=foo or foo=bar', rules => [ { values => { name => 'bob' }, operators => { name => '=' }, }, { values => { name => 'foo' }, operators => { name => '=' }, }, { values => { foo => 'bar' }, operators => { foo => '=' }, }, ], }, { string => 'name=bob and (score=2 or foo=bar)', rules => [ { values => { name => 'bob', score => 2, }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => 'bar' }, operators => { name => '=', foo => '=' }, }, ], }, { string => '(name=bob or name=joe) and (score = 2 or score = 4)', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', score => 4 }, operators => { name => '=', score => '=' }, }, { values => { name => 'joe', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'joe', score => 4 }, operators => { name => '=', score => '=' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => 'name = bob and (score=2 or foo=bar and (name in ["bob","fred","joe"] and score > -10.16))', rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => ['bob','fred','joe'], foo => 'bar', score => -10.16 }, operators => { name => 'in', foo => '=', score => '>' }, # calling values() will return 4 things (since name is in there twice), but value_for('name') returns the list override_value_count => 4, }, ], }, { string => q(name=bob and (score = 2 or (foo:"bar "/baz/' quux "quux" ' and (score!:-100.321--.123 or score<4321)))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => [-100.321, -0.123]}, operators => { name => '=', foo => 'in', score => 'not between' }, }, { values => { name => 'bob', foo => [' quux "quux" ', 'bar ','baz'], score => 4321 }, operators => { name => '=', foo => 'in', score => '<' }, }, ], }, { string => q( name=bob and (score = 2 or ( foo = bar and (parent_name=joe or ((group=cool or ritem.ritem_number<0.123) and (ritem_id = 123 or ritem.ritem_property=mojo)))))), rules => [ { values => { name => 'bob', score => 2 }, operators => { name => '=', score => '=' }, }, { values => { name => 'bob', foo => 'bar', parent_name => 'joe' }, operators => { name => '=', foo => '=', parent_name => '=' }, }, { values => { name => 'bob', foo => 'bar', group => 'cool', ritem_id => 123 }, operators => { name => '=', foo => '=', group => '=', ritem_id => '=' }, }, { values => { name => 'bob', foo => 'bar', group => 'cool', 'ritem.ritem_property' => 'mojo' }, operators => { name => '=', foo => '=', group => '=', 'ritem.ritem_property' => '=' }, }, { values => { name => 'bob', foo => 'bar', 'ritem.ritem_number' => 0.123, ritem_id => 123 }, operators => { name => '=', foo => '=', 'ritem.ritem_number' => '<', ritem_id => '=' }, }, { values => { name => 'bob', foo => 'bar', 'ritem.ritem_number' => 0.123, 'ritem.ritem_property' => 'mojo' }, operators => { name => '=', foo => '=', 'ritem.ritem_number' => '<', 'ritem.ritem_property' => '=' }, }, ], }, ) { $DB::single=1 if ($test->{'stop'}); my $string = $test->{'string'}; my $composite_rule = UR::BoolExpr->resolve_for_string('URT::Item',$string); ok($composite_rule, "Created rule from string \"$string\""); isa_ok($composite_rule->template, 'UR::BoolExpr::Template::Or'); #print Data::Dumper::Dumper($composite_rule); my @r = $composite_rule->underlying_rules(); is(scalar(@r), scalar(@{$test->{'rules'}}), 'Underlying rules count is correct'); for (my $i = 0; $i< @{ $test->{'rules'}}; $i++) { my $r = $r[$i]; my $test_rule = $test->{'rules'}->[$i]; my $values = $test_rule->{'values'}; my $value_count = $test_rule->{'override_value_count'} || scalar(values %$values); my @properties = keys %$values; my $operators = $test_rule->{'operators'}; my @got_values = $r->values(); is(scalar(@got_values), $value_count, "Composite rule $i has the right number of values"); foreach my $property (@properties) { is_deeply($r->value_for($property), $values->{$property}, "Value for $property is correct"); is($r->operator_for($property), $operators->{$property}, "Operator for $property is correct"); } } exit if ($test->{'stop'}); } foreach my $test ( { string => 'name in bob/fred and score<-2', exception => qr{Syntax error near token WORD 'bob/fred'.*Expected one of: LEFT_BRACKET}s, }, { string => 'name:[bob,fred] and score<-2', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'name:/a/path/name', exception => qr{Syntax error near token IN_DIVIDER '/'}, }, { string => 'score=[1,2,3]', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'score!=[1,2,3]', exception => qr{Syntax error near token LEFT_BRACKET '\['}, }, { string => 'name=foo order by -score desc', exception => qr{Syntax error near token DESC_WORD 'desc'}, }, { string => 'name=foo order by -score asc', exception => qr{Syntax error near token ASC_WORD 'asc'}, }, { string => 'name=foo order by score desc asc', exception => qr{Syntax error near token ASC_WORD 'asc'}, }, ) { my $string = $test->{'string'}; my $exception_re = $test->{'exception'}; my $r; eval { $r = UR::BoolExpr->resolve_for_string( 'URT::Item', $test->{'string'}); }; ok(!$r, "Correctly did not create rule from string \"$string\""); like($@, $exception_re, 'exception looks right'); } 1; 26_indirect_mutator_with_where_via_is_many.t000444023532023421 300712121654173 23653 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 5; class Person::Relationship { id_by => [ person_id => { is => 'Number', implied_by => 'person', }, related_id => { is => 'Number', implied_by => 'related' }, name => { is => 'Text', }, ], has => [ person => { is => 'Person', id_by => 'person_id', }, related => { is => 'Person', id_by => 'related_id' }, ], }; class Person { id_by => [ name => { is => 'Text', }, ], has => [ relationships => { is => 'Person::Relationship', reverse_as => 'person', is_many => 1, is_mutable => 1, is_optional => 1, }, best_friend => { is => 'Person', via => 'relationships', to => 'related', where => [ name => 'best friend', ], is_many => 0, is_mutable => 1, is_optional => 1, } ], }; my $george = Person->create( name => 'George Washington', ); ok($george, 'created George Washington'); my $john = Person->create( name => 'John Adams', ); ok($john, 'created John Adams'); my $james = Person->create( name => 'James Madison', best_friend => $george, ); ok($james, 'created James Madison'); is_deeply($james->best_friend, $george, 'James best friend is set to George in create'); $james->best_friend($john); is_deeply($james->best_friend, $john, 'James best friend is set to John'); 83_commit_between_schemas.t000444023532023421 1156212121654173 20230 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 17; use DBD::SQLite; print $DBD::SQLite::VERSION,"\n"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; use File::Temp; use URT::DataSource::SomeSQLite; # This tests a case where there are two tables, one in the default schema and one in # an attached schema, and there is a foreign key between the two tables requiring # UR::DataSource::RDBMS::_sync_database to process it as a prerequsite. There was a bug # that could cause data to get dropped on the floor in this case (fixed in commit da174c) our($tmp_file1, $tmp_file2); $tmp_file1 = File::Temp::tmpnam() . "_ur_testsuite_83_db1.sqlite3"; $tmp_file2 = File::Temp::tmpnam() . "_ur_testsuite_83_db2.sqlite3"; END { unlink $tmp_file1 if defined $tmp_file1; unlink $tmp_file2 if defined $tmp_file2; } my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); # A sqlite-ism way of pretending we have different schemas my $autocommit = $dbh->{'AutoCommit'}; $dbh->{'AutoCommit'} = 1; # I'd rather use memory DBs, but SQLite segfaults in commit() below ok($dbh->do("attach database '$tmp_file1' as PROD_DB"), 'defined PROD_DB schema'); ok($dbh->do("attach database '$tmp_file2' as PEOPLE"), 'defined PEOPLE schema'); $dbh->{'AutoCommit'} = $autocommit; ok($dbh->do('create table PEOPLE.PEOPLE ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created product table'); ok($dbh->do('create table PROD_DB.PRODUCT ( product_prod_id int NOT NULL PRIMARY KEY, product_name varchar, creator_id integer references PEOPLE(person_id))'), 'created product table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PEOPLE.PEOPLE', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for product creator'); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', id_by => [ prod_id => { is => 'NUMBER', sql => 'product_prod_id' }, ], has => [ name => { is => 'STRING', sql => 'product_name' }, creator => { is => 'URT::Person', id_by => 'creator_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); $dbh->commit(); # SQLite doesn't really do foreign key constraints, and really doesn't do them # between databases, so insert some metaDB info about a foreign key between # the product's creator and a person's ID sub URT::DataSource::SomeSQLite::owner { 'PROD_DB'; } # the default schema/owner sub URT::DataSource::SomeSQLite::get_foreign_key_details_from_data_dictionary { my $self = shift; my($fk_catalog,$fk_schema,$fk_table,$pk_catalog,$pk_schema,$pk_table) = @_; unless ($fk_table eq 'PRODUCT' or $pk_table eq 'PEOPLE') { return UR::DataSource::SQLite::get_foreign_key_details_from_data_dictionary($self,@_); } my $sponge = DBI->connect("DBI:Sponge:", '','') or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr"); my @returned_names = qw( FK_NAME UK_TABLE_NAME UK_COLUMN_NAME UK_TABLE_SCHEM FK_TABLE_NAME FK_COLUMN_NAME FK_TABLE_SCHEM ); my $table = $pk_table || $fk_table; my @ret_data = ( { FK_NAME => 'product_person_fk', UK_TABLE_SCHEM => 'PEOPLE', UK_TABLE_NAME => 'PEOPLE', UK_COLUMN_NAME => 'person_id', FK_TABLE_SCHEM => 'PROD_DB', FK_TABLE_NAME => 'PRODUCT', FK_COLUMN_NAME => 'creator_id' } ); my $returned_sth = $sponge->prepare("foreign_key_info $table", { rows => [ map { [ @{$_}{@returned_names} ] } @ret_data ], NUM_OF_FIELDS => scalar @returned_names, NAME => \@returned_names, }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr()); return $returned_sth; } my $person = URT::Person->create(person_id => 1, name => 'Bob'); ok($person, 'Created a person'); my $product = URT::Product->create(prod_id => 1, name => 'Jet Pack', creator => $person); ok($product, 'Created a product created by that person'); ok(UR::Context->commit, 'Commit'); my $data = $dbh->selectrow_hashref('select * from PROD_DB.PRODUCT where product_prod_id = 1'); ok($data, 'Got back data from the DB for the product'); is($data->{'product_prod_id'}, 1, 'product_id ok'); is($data->{'product_name'}, 'Jet Pack', 'name ok'); is($data->{'creator_id'}, 1, 'creator_id ok'); $data = $dbh->selectrow_hashref('select * from PEOPLE.PEOPLE where person_id = 1'); ok($data, 'Got back data from the DB for the creator'); is($data->{'person_id'}, 1, 'person_id ok'); is($data->{'name'}, 'Bob', 'name ok'); 77_file_undef_value_handling.t000444023532023421 1467012121654173 20672 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; # Test the different ways that File datasources handling of NULL might differ # with the way Perl and UR convert NULL to undef and the various # numeric and string conversions when doing comparisions. We want UR's # object cache to return the same results that a query against the database # would use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 226; use IO::File; use URT::DataSource::SomeFile; my $ds = URT::DataSource::SomeFile->get(); my $filename = $ds->server; my $fh = IO::File->new($filename, O_WRONLY|O_CREAT); ok($fh, 'Got file handle'); my $delim = $ds->delimiter; $fh->print(join($delim, 1,'',''),"\n"); $fh->print(join($delim, 2,'',''),"\n"); ok($fh->close(),'Write file data'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has_optional => [ value => { is => 'Integer', column_name => 'thing_name' }, color => { is => 'String', column_name => 'thing_color' }, ], data_source => 'URT::DataSource::SomeFile', table_name => 'things', ); my @result; # For the equality operator, "value => undef" is converted to SQL as # "value IS NULL", not "value = NULL, so it should return the items foreach my $value ( undef ) { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } foreach my $value ( '') { # undef and the empty string both mean NULL @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef loaded 2 items'); @result = URT::Thing->get(value => $value); is(scalar(@result), 2, 'value => undef returned all 2 items'); URT::Thing->unload(); # clear object and query cache } # For other values using the equality operator, it should return nothing foreach my $value ( 0, 1, -1) { operator_returns_object_count('', $value,0); } ## != for non-null values should return both things foreach my $value ( 0, 1, -1) { my @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => '!=', value => $value}); is(scalar(@result), 2, "value != $value (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) loaded 2 items"); @result = URT::Thing->get('value !=' => $value); is(scalar(@result), 2, "value != $value (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } # the 'false' operator should return both things, since NULL is false { my @result = URT::Thing->get(value => { operator => 'false', value => '' }); is(scalar(@result), 2, "value is false (old syntax) loaded 2 items"); @result = URT::Thing->get(value => { operator => 'false', value => ''}); is(scalar(@result), 2, "value is false (old syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) loaded 2 items"); @result = URT::Thing->get('value false' => 1); is(scalar(@result), 2, "value is false (new syntax) returned 2 items"); URT::Thing->unload(); # clear object and query cache } foreach my $operator ( qw( < <= > >= true ) ) { foreach my $value ( undef, 0, "", 1, -1) { operator_returns_object_count($operator,$value,0); last if ($operator eq 'true' or $operator eq 'false'); # true and false don't use the 'value' anyway } } # FIXME - uninitialized warnings here foreach my $operator ( 'like', 'not like' ) { foreach my $value ( undef, '%', '%1', '%1%' ) { operator_returns_object_count($operator, $value, 0) } } # 'in' operator # value => [undef] does SQL to include NULL items operator_returns_object_count('in', [undef], 2); operator_returns_object_count('not in', [undef], 0); foreach my $operator ( '', 'in', 'not in' ) { foreach my $value ( [], [1] ) { operator_returns_object_count($operator, $value, 0); } } # 'between' operator foreach my $value ( [undef, undef], [1,1], [0,1], [-1,0], [-1,-1], [undef, 1], [undef, 0], [undef, -1], [1, undef], [0, undef], [-1, undef] ) { operator_returns_object_count('between', $value, 0); } sub operator_returns_object_count { my($operator,$value,$expected_count) = @_; if (ref($value) eq 'ARRAY' and !$operator) { $operator = 'in'; } my $print_operator = $operator || '=>'; my $print_value; if (! defined $value) { $print_value = '(undef)'; } elsif (length($value) == 0 ) { $print_value = '""'; } elsif (ref($value) eq 'ARRAY') { $print_value = '[' . join(",", map { defined($_) ? "'$_'" : '(undef)' } @$value) . ']'; } else { $print_value = $value; } # Original non-eq-operator syntax @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get(value => { operator => $operator, value => $value }); is(scalar(@result), $expected_count, "value $print_operator $print_value (old syntax) returns $expected_count item(s)"); URT::Thing->unload(); # New syntax my $property_string = "value $operator"; @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) loads $expected_count item(s)"); URT::Thing->unload(); # clear object and query cache URT::Thing->get(1); # Get an object into the cache @result = URT::Thing->get($property_string => $value); is(scalar(@result), $expected_count, "value $print_operator $print_value (new syntax) returns $expected_count item(s)"); URT::Thing->unload(); } 10_accessor_object.t000444023532023421 465712121654173 16631 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 7; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; UR::Object::Type->define( class_name => 'Acme', is => 'UR::Namespace' ); UR::Object::Type->define( class_name => 'Acme::Manufacturer', id_by => ['name'], has => [qw/name industry/], ); my $m1 = Acme::Manufacturer->create(name => "Lockheed Martin"); my $m2 = Acme::Manufacturer->create(name => "Boeing"); my $m3 = Acme::Manufacturer->create(name => "Explosives R US"); UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name' => {}, 'manufacturer' => { type => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius' => {}, #'manufacturer_name' => { via => 'manufacturer', to => 'name' }, ] ); my $p1 = Acme::Product->create(name => "jet pack", genius => 6, manufacturer => $m1); my $p2 = Acme::Product->create(name => "hang glider", genius => 4, manufacturer => $m2); Acme::Product->create(name => "mini copter", genius => 5, manufacturer => $m2); Acme::Product->create(name => "firecracker", genius => 6, manufacturer => $m3); Acme::Product->create(name => "dynamite", genius => 7, manufacturer => $m3); Acme::Product->create(name => "plastique", genius => 8, manufacturer => $m3); my @obj = Acme::Product->get(); is(scalar(@obj), 6, "got the expected objects"); #ok(Acme::Product->can("manufacturer"), "the object-accessor is present"); is(Acme::Product->get(name => "jet pack")->manufacturer->name, "Lockheed Martin", "object accessor works"); is(Acme::Product->get(name => "dynamite")->manufacturer->name, "Explosives R US", "object accessor works"); my $jetpack = Acme::Product->get(name => "jet pack"); ok($jetpack->manufacturer($m2), 'Change manufacturer on jet pack'); is($jetpack->manufacturer->name, 'Boeing', 'Change was successful'); eval { $jetpack->manufacturer('Boeing') }; ok($@, 'Setting the object accessor to a string throws an exception'); like($@, qr(Can't call method "id" without a package or object reference. Expected an object as parameter to 'manufacturer', not the value 'Boeing'), 'The exception was correct'); #is(Acme::Product->get(name => "jet pack")->manufacturer_name, "Lockheed Martin", "delegated accessor works"); #is(Acme::Product->get(name => "dynamite")->manufacturer_name, "Explosives R US", "delegated accessor works"); 03h_rule_for_property_meta.t000444023532023421 136612121654173 20434 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; class My::Foo { attributes_have => [ is_blah => { is => 'Boolean' }, ], has => [ foo => { is => 'Text', is_blah => 1 }, bar => { is => 'Text', is_blah => 0 }, ] }; my $meta = My::Foo->__meta__; my @p; @p = $meta->properties(is_blah => 1); is(scalar(@p), 1, "got just one property"); is($p[0]->property_name, "foo", "got the expected property"); @p = $meta->properties(is_blah => 0); is(scalar(@p), 1, "got just one property"); is($p[0]->property_name, "bar", "got the expected property"); 69_subclassify_by_db.t000444023532023421 1165212121654173 17216 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::DataSource::SomeSQLite; use Test::More tests => 41; my $dbh = URT::DataSource::SomeSQLite->get_default_handle;; ok($dbh, 'Got database handle'); # Employees are subclassed into eith Workers or Bosses. # workers have no additional table, but bosses do ok($dbh->do('create table EMPLOYEE (employee_id integer NOT NULL PRIMARY KEY, name varchar NOT NULL, subclass_name varchar NOT NULL)'), 'create employee table'); ok($dbh->do('create table BOSS (boss_id integer NOT NULL PRIMARY KEY REFERENCES employee(employee_id), office varchar)'), 'create boss table'); # odd numbered employees are workers, evens are bosses my $insert_emp = $dbh->prepare('insert into employee values (?,?,?)'); my $insert_boss = $dbh->prepare('insert into boss values (?,?)'); foreach my $id ( 1 .. 10 ) { if ($id % 2) { # odd $insert_emp->execute($id, 'Bob '.$id, 'URT::Worker'); } else { $insert_emp->execute($id, 'Bob '.$id, 'URT::Boss'); $insert_boss->execute($id, $id); } } $insert_emp->finish; $insert_boss->finish; UR::Object::Type->define( class_name => 'URT::Employee', subclassify_by => 'subclass_name', is_abstract => 1, id_by => 'employee_id', has => [ name => { type => "String" }, subclass_name => { type => 'String' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Worker', is => 'URT::Employee', ); UR::Object::Type->define( class_name => 'URT::Boss', is => 'URT::Employee', id_by => 'boss_id', has => [ office => { is => 'String' }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); my @query_text; my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {push @query_text, $_[2]; $query_count++}), 'Created a subscription for query'); @query_text = (); $query_count = 0; my $o = URT::Employee->get(1); ok($o, 'Got employee with id 1'); isa_ok($o,'URT::Worker'); is($query_count, 1, 'Made one query'); like($query_text[0], qr(from EMPLOYEE), 'Query hits the EMPLOYEE table'); unlike($query_text[0], qr(where subclass_name), 'Query does not filter by subclass_name'); unlike($query_text[0], qr(from BOSS), 'Query does not hit the BOSS table'); @query_text = (); $query_count = 0; $o = URT::Worker->get(3); ok($o, 'Got worker with id 3'); isa_ok($o,'URT::Worker'); is($query_count, 1, 'Made one query'); like($query_text[0], qr(from EMPLOYEE), 'Query hits the EMPLOYEE table'); like($query_text[0], qr(EMPLOYEE.subclass_name), 'Query filters by subclass_name'); unlike($query_text[0], qr(from BOSS), 'Query does not hit the BOSS table'); @query_text = (); $query_count = 0; $o = URT::Employee->get(2); ok($o, 'Got employee with id 2'); isa_ok($o,'URT::Boss'); is($query_count, 2, 'Made 2 queries'); like($query_text[0], qr(from EMPLOYEE), 'first query selects from EMPLOYEE table'); unlike($query_text[0], qr(BOSS), 'first query does not touch the BOSS table'); unlike($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'first query does not filter by subclass_name'); like($query_text[1], qr(from BOSS), 'second query selects from the BOSS table'); like($query_text[1], qr(INNER join EMPLOYEE), 'second query joins to the EMPLOYEE table'); unlike($query_text[1], qr(EMPLOYEE.subclass_name = \?), 'second query does not filter by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Boss->get(4); ok($o, 'Got boss with id 4'); isa_ok($o,'URT::Boss'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(from BOSS), 'Query selects from BOSS table'); like($query_text[0], qr(INNER join EMPLOYEE), 'query joins to the EMPLOYEE table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Worker->get(6); ok(!$o, 'Did not find a Worker with id 6'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(from EMPLOYEE), 'query selects from EMPLOYEE table'); unlike($query_text[0], qr(BOSS), 'query does not mention BOSS table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); @query_text = (); $query_count = 0; $o = URT::Boss->get(7); ok(!$o, 'Did not find a boss with id 6'); is($query_count, 1, 'Made 1 query'); like($query_text[0], qr(INNER join EMPLOYEE), 'query joins to EMPLOYEE table'); like($query_text[0], qr(from BOSS), 'query selects from BOSS table'); like($query_text[0], qr(EMPLOYEE.subclass_name = \?), 'query filters by subclass_name'); 04e_file.t000444023532023421 1007612121654173 14600 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 54; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace # FIXME - this doesn't test the UR::DataSource::File internals like seeking and caching my $ds = URT::DataSource::SomeFile->get(); my $filename = $ds->server; ok($filename, 'URT::DataSource::SomeFile has a server'); unlink $filename if -f $filename; my $rs = $ds->record_separator; our @data = ( [ 1, 'Bob', 'blue' ], [ 2, 'Fred', 'green' ], [ 3, 'Joe', 'red' ], [ 4, 'Frank', 'yellow' ], ); &setup($ds); my $fh = $ds->get_default_handle(); ok($fh, "got a handle"); isa_ok($fh, 'IO::Handle', 'Returned handle is the proper class'); my $thing = URT::Things->get(thing_name => 'Fred'); ok($thing, 'singular get() returned an object'); is($thing->id, 2, 'object id is correct'); is($thing->thing_id, 2, 'thing_id is correct'); is($thing->thing_name, 'Fred', 'thing_name is correct'); is($thing->thing_color, 'green', 'thing_color is correct'); #my @things = URT::Things->get('thing_color ne' => 'red'); my @things = URT::Things->get(thing_color => {operator => 'not in', value => ['red','green']}); is(scalar(@things), 2, 'Get where color ne "red" returned 3 items'); @things = URT::Things->get(thing_color => { operator => 'like', value => 'ye%o%' }); is(scalar(@things), 1, 'Returned one thing for "thing_color like" "ye%o%"'); is($things[0]->thing_name, 'Frank', 'It was the right thing'); @things = URT::Things->get(); is(scalar(@things), scalar(@data), 'multiple get() returned the right number of objects'); for (my $i = 0; $i < @data; $i++) { # They should get returned in the same order, since @data is sorted is($things[$i]->thing_id, $data[$i]->[0], "Object $i thing_id is correct"); is($things[$i]->thing_name, $data[$i]->[1], "Object $i thing_name is correct"); is($things[$i]->thing_color, $data[$i]->[2], "Object $i thing_color is correct"); } my $iter1 = URT::Things->create_iterator(); my $iter2 = URT::Things->create_iterator(); for (my $i = 0; $i < @data; $i++) { my $obj = $iter1->next(); is($obj->thing_id, $data[$i]->[0], 'Iterator 1, thing_id is correct'); is($obj->thing_name, $data[$i]->[1], 'Iterator 1, thing_name is correct'); is($obj->thing_color, $data[$i]->[2], 'Iterator 1, thing_color is correct'); $obj = $iter2->next(); is($obj->thing_id, $data[$i]->[0], 'Iterator 2, thing_id is correct'); is($obj->thing_name, $data[$i]->[1], 'Iterator 2, thing_name is correct'); is($obj->thing_color, $data[$i]->[2], 'Iterator 2, thing_color is correct'); } my $obj = $iter1->next(); ok(! defined($obj), 'Iterator 1 returns undef when all data is exhausted'); $obj = $iter2->next(); ok(! defined($obj), 'Iterator 2 returns undef when all data is exhausted'); my $fh2 = $ds->get_default_handle(); my $thing1 = URT::Things->get(thing_name => 'FredX'); my $pid = UR::Context::Process->fork(); if ($pid) { my $thing2= URT::Things->get(thing_name => 'FredY'); ok(!$thing2, "correctly failed to get something we didn't expect to see"); ok(URT::Things->get(thing_color=>'yellow'), "got something we did expect to see, even after forking"); waitpid($pid, 0); } else { sleep 3; exit(0); } unlink URT::DataSource::SomeFile->server; sub setup { my $ds = shift; my $filename = $ds->server; my $fh = IO::File->new($filename, '>'); ok($fh, 'opened file for writing'); my $delimiter = $ds->delimiter; my $rs = $ds->record_separator; foreach my $line ( @data ) { $fh->print(join($delimiter, @$line),$rs); } $fh->close; my $c = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, ], table_name => 'FILE', data_source => 'URT::DataSource::SomeFile' ); ok($c, 'Created class'); } 1; 20_has_many.t000444023532023421 1000412121654173 15300 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 22; UR::Object::Type->define( class_name => 'Acme', is => 'UR::Namespace' ); UR::Object::Type->define( class_name => 'Acme::Order', table_name => 'order_', id_by => [ order_id => { is => 'integer', is_optional => 1, column_name => 'order_id' }, ], has_many => [ lines => { is => 'Acme::OrderLine' }, line_quantities => { via => 'lines', to => 'quantity' }, ], ); UR::Object::Type->define( class_name => 'Acme::OrderBuddy', id_by => [ order => { is => 'Acme::Order', id_by => 'order_id', constraint_name => 'order_line' }, line_num => { is => 'Integer', is_optional => 1, column_name => 'line_num' }, ], ); UR::Object::Type->define( class_name => 'Acme::OrderLine', table_name => 'order_line', id_by => [ order => { is => 'Acme::Order', id_by => 'order_id', constraint_name => 'order_line' }, line_num => { is => 'Integer', is_optional => 1, column_name => 'line_num' }, ], has => [ quantity => { is => 'Integer', is_optional => 1, column_name => 'quantity' }, product => { is => 'String', constraint_name => 'order_line_product' }, ], ); my $o = Acme::Order->create(id => 1); ok($o, "order object created"); my $line1 = Acme::OrderLine->create(order => $o, line_num => 1, quantity => 100, product => "fish"); my $line2 = Acme::OrderLine->create(order => $o, line_num => 2, quantity => 200, product => "fish"); my $line3 = Acme::OrderLine->create(order => $o, line_num => 3, quantity => 300, product => "fish"); my @lines = sort Acme::OrderLine->get(order => $o); is(scalar(@lines), 3, "created expected list of 3 line items"); ok($o->can("line"), "can do line"); ok($o->can("lines"), "can do lines"); ok($o->can("line_list"), "can do line_list"); ok($o->can("line_arrayref"), "can do line_arrayref"); ok($o->can("add_line"), "can do add_line"); ok($o->can("remove_line"), "can do remove_line"); my @r1 = sort $o->lines(); is_deeply(\@r1,\@lines,"lines() works"); my @q1 = sort $o->line_quantities; my @q1_expected = sort map { $_->quantity } @lines; is_deeply(\@q1,\@q1_expected,"indirect method (line_quantities()) returns lists through the lines() acccessor"); my @r2 = sort $o->line_list(); is_deeply(\@r2,\@lines,"line_list() works"); my @r3 = sort @{ $o->line_arrayref() }; is_deeply(\@r3,\@lines,"line_arrayref() works"); my @r4; eval { @r4 = $o->line(line_num => 1) }; is($r4[0], $line1, "line() works with a simple rule"); my $r5; eval { $r5 = $o->line(2) }; is($r5, $line2, "line() returns a single selected item"); my $line4 = $o->add_line(line_num => 4, quantity => 400, product => "fish"); ok($line4, "added a line with full additional parameters"); my @r6 = sort { $a->line_num <=> $b->line_num } $o->lines(); is(scalar(@r6),4,"line count is correct"); my $line5 = $o->add_line(5); ok($line5, "added a line with a partial identity"); my @r7 = sort { $a->line_num <=> $b->line_num } $o->lines(); is(scalar(@r7),5,"line count is correct"); $line5->product('fish'); # Sets the property's value, since it's not is_optional my $removed = $o->remove_line(3); ok($removed, "removed a line with a partial identity"); my @r8 = sort map { $_->line_num } $o->lines(); is("@r8","1 2 4 5","line numbers left are correct"); my $removed2 = $o->remove_line(quantity => 400); ok($removed2, "removed a line with full parameters"); my @r9 = sort map { $_->line_num } $o->lines(); is("@r9","1 2 5","line numbers left are correct"); =cut # This only works if there is a data source currently, # since the whole closure logic is inside of UR::DataSource::RDBMS. ok($o->can("line_iterator"), "can do line_iterator"); my $i = $o->line_iterator; ok($i, "got an iterator"); my @o4; if ($i) { while (my $next = $i->next) { push @o4, $next; } } is_deeply(\@o4,\@lines,"line_iterator works"); =cut 1; 95_detect_db_deleted.t000444023532023421 2153712121654173 17135 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 49; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table thing (thing_id integer PRIMARY KEY, value varchar, other varchar)'); my $sth = $dbh->prepare('insert into thing values (?,?,?)'); foreach my $id ( 2..10 ) { $sth->execute($id,chr($id+64), chr($id+64)); # id 2 has balue B, id 3 has value C, etc } $sth->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['value','other'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # A thing we've defined but does not exist in the DB my $defined_thing = URT::Thing->__define__(thing_id => '12345', value => 'CC', other => 'CC'); ok($defined_thing, 'Instantiate a URT::Thing with __define__'); # At the start, there are 9 items in the DB, plus the __define__d one my @things = URT::Thing->get(); is(scalar(@things), 10, 'Got all 10 things'); ok($dbh->do('delete from thing where thing_id = 4'), 'Delete thing_id 4 from the database'); # Deleted one, now there are 9 @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 9, 'reload() returned 9 things'); ok($dbh->do('delete from thing where thing_id = 6'), 'Delete thing_id 6 from the database'); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 8, 'get() returned 8 things'); # Change object 2's value from 'B' to ZZZ. Now there are 8 things in memory and the DB. # In the DB, object 2's value sorts first, but in memory it sorts last # Remaing object IDs should be 2,3,5,7,8,9,10,12345 my $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(2)->value('ZZZ'), "Change thing 2's value to ZZZ"); my @expected_ids = (3,12345,5,7,8,9,10,2); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 8, 'Got 8 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete object 2 from the DB. Reloading will throw an exception # because it's modified in memory, and modified in a conflicting manner in the # database (it's deleted) ok($dbh->do('delete from thing where thing_id = 2'), 'Delete thing_id 2 from database'); @things = eval { UR::Context->reload('URT::Thing', -order => ['value']) }; is(scalar(@things),0, 'Got no things back from reload()'); like($@, qr(URT::Thing ID '2' previously existed in an underlying), 'reload thew an exception about the deleted object'); # Undo the previous change so we won't get an exception again $trans->rollback; # After rolling back the transaction, Object ID 2 is still deleted from the database, # but exists in memory as an unchanged object. There are now 6 objects in the DB # and 8 in memory (don't forget the 1 that was __define_d # # Now, change object 10's value from J to A in memory. In the DB, object 10 sorts last, # but in memory it sorts first. $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(10)->value('A'), 'Change thing id 10 value to A'); @expected_ids = (10,3,12345,5,7,8,9); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 7, 'Got 7 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete it from the DB and make sure it throws an exception ok($dbh->do('delete from thing where thing_id = 10'), 'Delete thing_id 10 from database'); @things = eval { UR::Context->reload('URT::Thing', -order => ['value']) }; is(scalar(@things),0, 'Got no things back from reload()'); like($@, qr(URT::Thing ID '10' previously existed in an underlying), 'reload thew an exception about the deleted object'); $trans->rollback; # After the transaction, object ID 10 is still deleted from the database # but exists in memory as an unchanged object. There are now 5 objects in # the DB and 7 in memory (6 that came from the DB, plus the __define__d one) # Change object 3's value from C to ZZZ in the DB. In memory it will sort first, # but in the database it will sort last. $trans = UR::Context::Transaction->begin; ok($dbh->do("update thing set value = 'ZZZ' where thing_id = 3"), 'Change thing id 3 value to ZZZ in the database'); @expected_ids = (12345,5,7,8,9,3); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 6 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Now delete the object ok(URT::Thing->get(3)->delete, 'Delete thing id 3 from memory'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 5, 'Got 4 object back from reload'); @expected_ids = (12345,5,7,8,9); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); $trans->rollback; # Change object 9's value from I to A in the DB. In memory it will sort # last, but in the DB it will sort first. Object 3 still has value 'ZZZ' $trans = UR::Context::Transaction->begin; ok($dbh->do("update thing set value = 'A' where thing_id = 9"), 'Change thing id 9 value to A in the database'); @expected_ids = (9,12345,5,7,8,3); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 6 things ordered by value'); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # now delete object ID 9 ok(URT::Thing->get(9)->delete, 'Delete thing id 9 from memory'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 5, 'Got 4 object back from reload'); @expected_ids = (12345,5,7,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); # Hack required in UR::Context::__merge... to get object 9 to correctly have # value => 'A' instead of 'I' $trans->rollback; # Try changing an unrelated property and do a query # # Object 9 is back again with value 'A' because of the rollback. $trans = UR::Context::Transaction->begin; ok(URT::Thing->get(7)->other('blahblah'), 'Change thing id 7 "other" property'); ok($dbh->do("update thing set other = 'foofoo' where thing_id = 8"), 'Change thing id 8 "other" property in the database'); @things = UR::Context->reload('URT::Thing', -order => ['value']); is(scalar(@things), 6, 'Got 4 objects back from reload'); @expected_ids = (9,12345,5,7,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); $trans->rollback; # Make a change to both an order-by and a filtered property in memory $trans = UR::Context::Transaction->begin; my $obj = URT::Thing->get(7); ok($obj->other('blahblah'), 'Change object 7s other property to blahblah'); ok($obj->value('A'), 'Change object 7s value to A'); @things = UR::Context->reload('URT::Thing', 'other ne' => 'blahblah', -order => ['value']); is(scalar(@things), 5, 'Got back 5 things from reload() where other is not blahblah'); @expected_ids = (9,12345,5,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); isa_ok($obj, 'URT::Thing', 'Thing id 7 was not deleted'); $trans->rollback; # Make a change to both an order-by and filtered property in the DB ok($dbh->do("update thing set other = 'blahblah' where thing_id = 7"), 'Change thing id 7 "other" property in the database'); ok($dbh->do("update thing set value = 'A' where thing_id = 7"), 'Change thing id 7 value to "A" in the database'); @things = UR::Context->reload('URT::Thing', 'other ne' => 'blahblah', -order => ['value']); is(scalar(@things), 5, 'Got back 5 things from reload() where other is not blahblah'); @expected_ids = (9,12345,5,8,3); is_deeply([ map { $_->id } @things], \@expected_ids, 'Objects came back in the expected order'); ok(URT::Thing->get(7), 'Thing id 7 was not deleted'); ok($dbh->do('delete from thing'), 'Delete all remaining things from the database'); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 1, 'reload() returned one thing'); is($things[0]->id, $defined_thing->id, 'It was the thing we defined at the beginning of the test'); ok($defined_thing->delete, "Delete the defined object"); @things = UR::Context->reload('URT::Thing'); is(scalar(@things), 0, 'reload() returned no objects'); 50_force_always_reload.t000444023532023421 717712121654173 17511 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More skip_all => 'in development'; #tests => 34; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; &setup_classes_and_db($dbh); is(UR::Context->current->query_underlying_context, undef, 'Initial value for query_underlying_context is undef'); my $query_count = 0; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); UR::Context->current->query_underlying_context(1); $query_count = 0; my $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1'); is($query_count,1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1 again'); is($query_count,1, 'Made 1 query again'); $query_count = 0; my @things = URT::Thing->get('id <' => 100); is(scalar(@things), 3, 'Got all 3 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get('id <' => 100); is(scalar(@things), 3, 'Got all 3 things again'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing id 1 again'); is($query_count,1, 'Made 1 query again'); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count,1, 'Made 1 query again'); $query_count = 0; $thing = URT::Thing->get(4); ok(! $thing, 'No thing with ID 4'); is($query_count,1, 'Made 1 query again'); UR::Context->current->query_underlying_context(undef); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count, 0, 'Made no queries because query_underlying_context is undef'); $query_count = 0; $thing = URT::Thing->get(4); ok(! $thing, 'No thing with ID 4'); is($query_count, 0, 'Made no queries because query_underlying_context is undef and query was done before'); ok($dbh->do("insert into thing values (10, 'Bubba', 'Person', 'red')"), 'insert new row into table'); $query_count = 0; @things = URT::Thing->get('id <' => 100); is(scalar(@things), 4, 'There are now 4 things'); is($query_count, 1, 'Made 1 query, even though get() was done before'); UR::Context->current->query_underlying_context(0); $query_count = 0; $thing = URT::Thing->get(2); ok($thing, 'Got thing id 2'); is($query_count, 0, 'Made no queries, query_underlying_context is 0'); $query_count = 0; $thing = URT::Thing->get(5); ok(! $thing, 'No thing with ID 5'); is($query_count, 0, 'Made no queries because query_underlying_context is 0'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 4, 'Got all 4 things'); is($query_count, 0, 'Made no queries because query_underlying_context is 0'); sub setup_classes_and_db { my $dbh = shift; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created thing table'); my $ins_things = $dbh->prepare("insert into thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a thing'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'color', 'type' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 87a_many_to_many_query_is_efficient.t000444023532023421 754412121654173 22304 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__).'/../../../lib'; use lib File::Basename::dirname(__FILE__).'/../..'; # Tests that for two entities with bridge objects connecting them one can # efficiently retrieve all of the associated entities across the bridge use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table CLUB ( club_id int NOT NULL PRIMARY KEY, name varchar )'), 'created club table'); ok($dbh->do('create table MEMBERSHIP ( membership_id int NOT NULL PRIMARY KEY, person_id int references PERSON(person_id), club_id int references CLUB(club_id))'), 'created membership table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, memberships => { is => 'URT::Membership', is_many => 1, reverse_as => 'member' }, clubs => { is => 'URT::Club', is_many => 1, via => 'memberships', to => 'club' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Club', table_name => 'CLUB', id_by => [ club_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, memberships => { is => 'URT::Membership', is_many => 1, reverse_as => 'club' }, members => { is => 'URT::Person', is_many => 1, via => 'memberships', to => 'member' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for clubs'); ok(UR::Object::Type->define( class_name => 'URT::Membership', table_name => 'MEMBERSHIP', id_by => [ membership_id => { is => 'NUMBER' }, ], has => [ person_id => { is => 'NUMBER' }, member => { is => 'URT::Person', id_by => 'person_id' }, club_id => { is => 'CLUB' }, club => { is => 'URT::Club', id_by => 'club_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); #insert data #Alice, Bob, and Charlie are members of Club A #Alice, Charlie, and Darlene are members of Club B #Alice and Charlie are members of Club C #Alice is a member of Club D my $insert = $dbh->prepare('insert into person values (?,?)'); for my $row ([1, 'Alice'], [2, 'Bob'], [3, 'Charlie'], [4, 'Darlene']) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into club values (?,?)'); for my $row ([100, 'Club A'], [200, 'Club B'], [300, 'Club C'], [400, 'Club D']) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into membership values (?,?,?)'); for my $row ([101, 1, 100], [102, 2, 100], [103, 3, 100], [201, 1, 200], [203, 3, 200], [204, 4, 200], [301, 1, 300], [303, 3, 300], [401, 1, 400], ){ $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_text = $_[0]; $query_count++} ), 'created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @clubs = $person->clubs(); is(scalar(@clubs), 4, 'got all 4 clubs of which person is a member'); is($query_count, '2', 'made 2 queries total'); #one to get memberships, one to get clubs my $club = URT::Club->get(200); ok($club, 'Got club object'); $query_count = 0; my @members = $club->members(); is(scalar(@members), 3, 'got all 3 members of the club'); is($query_count, '2', 'made 2 queries total'); #one to get memberships, one to get members 49b_complicated_get_2.t000444023532023421 1064212121654173 17232 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - the final property/column we're filtering on is on the parent class of the delegation &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_name => 'Collie'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id))"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id))"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name) values (?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart) values (?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id) values (?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id) values (?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie'); $breed_insert->execute(1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie'); $animal_insert->execute(2, 1); # a person named Jeff $name_insert->execute(3, 'Jeff'); $person_insert->execute(3,2); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => 'named_thing', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'breed', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); } 04e_file_sync_database.t000444023532023421 603212121654173 17435 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 20; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # dummy namespace # FIXME - this doesn't test the UR::DataSource::File internals like seeking and caching my $ds = URT::DataSource::SomeFile->get(); ok($ds, 'Got SomeFile data source'); &setup($ds); my $thing1 = URT::Things->get(thing_name => 'Fred'); ok($thing1, 'singular get() returned an object'); ok($thing1->thing_color('blueish'), 'Changed color'); my $thing2 = URT::Things->get(thing_name => 'Frank'); ok($thing2->thing_name('Anonymous'), 'Changed name on a different thing'); my $thing3 = URT::Things->get(thing_name => 'Joe'); ok($thing3->delete, 'Deleted a third thing'); my $new_thing1 = URT::Things->create(thing_id => 3, thing_name => 'Newby', thing_color=> 'clear'); ok($new_thing1, 'created new thing'); ok(!exists($new_thing1->{'db_committed'}), "New thing correctly has no 'db_committed' hash key"); my $new_thing2 = URT::Things->create(thing_id => 0, thing_name => 'Something', thing_color => 'white'); ok($new_thing2, 'created new thing 2'); my $new_thing3 = URT::Things->create(thing_id => 10, thing_name => 'Bobish', thing_color => 'redish'); ok($new_thing3, 'created new thing 3'); ok(UR::Context->commit, 'Commit'); &check_file($ds); ok(exists($new_thing1->{'db_committed'}), "New thing 1 now has a 'db_committed' has key"); unlink $ds->server; sub setup { my $ds = shift; my $filename = $ds->server; my $delimiter = $ds->delimiter; my $rs = $ds->record_separator; ok($filename, 'URT::DataSource::SomeFile has a server'); unlink $filename if -f $filename; my @data = ( [ 1, 'Bob', 'blue' ], [ 2, 'Fred', 'green' ], [ 4, 'Joe', 'red' ], [ 5, 'Frank', 'yellow' ], ); my $fh = IO::File->new($filename, '>'); ok($fh, 'opened file for writing'); foreach my $line ( @data ) { $fh->print(join($delimiter, @$line),$rs); } $fh->close; my $c = UR::Object::Type->define( class_name => 'URT::Things', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, ], table_name => 'FILE', data_source => 'URT::DataSource::SomeFile' ); ok($c, 'Created class'); } sub check_file { my $ds = shift; my $fh = IO::File->new($ds->server); my $line = $fh->getline(); is($line, qq(0\tSomething\twhite\n), 'Line 0 ok'); $line = $fh->getline(); is($line, qq(1\tBob\tblue\n), 'Line 1 ok'); $line = $fh->getline(); is($line, qq(2\tFred\tblueish\n), 'Line 2 ok'); $line = $fh->getline(); is($line, qq(3\tNewby\tclear\n), 'Line 3 ok'); $line = $fh->getline(); is($line, qq(5\tAnonymous\tyellow\n), 'Line 4 ok'); $line = $fh->getline(); is($line, qq(10\tBobish\tredish\n), 'Line 5 ok'); } 1; 91b_sets_count_with_changes.t000444023532023421 1652712121654173 20604 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 51; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); my $aggr_query_count = 0; my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { my ($observed, $aspect, $data) = @_; if ($data =~ /count|sum|min|max/) { $aggr_query_count++ } $query_count++; }), 'Created a subscription for query'); # Test creating/deleting/modifying objects that match extant sets. $query_count = 0; my $uncool_person_set = URT::Person->define_set(is_cool => 0); ok($uncool_person_set, 'Defined set of people that are not cool'); my $cool_person_set = URT::Person->define_set(is_cool => 1); ok($cool_person_set, 'Defined set of people that are cool'); is($cool_person_set->is_cool, 1, "access to a defining property works"); is($query_count, 0, 'Made no queries'); # Test set-relaying. my $car_set = $cool_person_set->cars_set; ok($car_set, "got a set of cars for the person set: object set -> value set"); # Test aggregate function on a set that has no member changes. # All aggregate functions should trigger query since function is # performed server-side on the data source. { ok(!$cool_person_set->_members_have_changes, 'cool set has no changed objects'); $aggr_query_count = 0; is($cool_person_set->count, 3, '3 people are cool'); is($aggr_query_count, 1, 'count triggered one query'); $aggr_query_count = 0; is($cool_person_set->min('age'), 25, 'determined min age'); is($aggr_query_count, 1, 'min triggered one query'); $aggr_query_count = 0; is($cool_person_set->max('age'), 45, 'determined max age'); is($aggr_query_count, 1, 'max triggered one query'); $aggr_query_count = 0; is($cool_person_set->sum('age'), 110, 'determined the sum of all ages of the set'); is($aggr_query_count, 1, 'sum triggered one query'); } # Now induce a change in a member and ensure no queries are performed. { my $p = URT::Person->get(11); ok($cool_person_set->rule->evaluate($p), 'person is member of cool person set'); ok($p->age($p->age + 1), 'changed the age of the youngest person to be +1 (26)'); ok($cool_person_set->_members_have_changes, 'cool person set no has changes'); $aggr_query_count = 0; is($cool_person_set->count, 3, 'set membership count is still the same'); is($aggr_query_count, 0, 'count did not trigger query'); $aggr_query_count = 0; is($cool_person_set->min('age'), 26, 'minimum age is now 26'); is($aggr_query_count, 0, 'min did not trigger query'); $aggr_query_count = 0; is($cool_person_set->max('age'), 45, 'maximum age is still 45'); is($aggr_query_count, 0, 'max did not trigger query'); $aggr_query_count = 0; is($cool_person_set->sum('age'), 111, 'the sum of all ages is now 111'); is($aggr_query_count, 0, 'sum did not trigger query'); } # Now ensure that a set with same member class but without any actual # member changes is not affected. { is($uncool_person_set->member_class_name, $cool_person_set->member_class_name, 'sets have the same member class'); isnt($uncool_person_set, $cool_person_set, 'sets are not the same'); ok(!$uncool_person_set->_members_have_changes, 'uncool set has no changed objects'); $aggr_query_count = 0; is($uncool_person_set->count, 2, 'set membership count is still the same'); is($aggr_query_count, 1, 'count triggered one query'); $aggr_query_count = 0; is($uncool_person_set->min('age'), 30, 'minimum age is now 30'); is($aggr_query_count, 1, 'min triggered one query'); $aggr_query_count = 0; is($uncool_person_set->max('age'), 35, 'maximum age is still 35'); is($aggr_query_count, 1, 'max triggered one query'); $aggr_query_count = 0; is($uncool_person_set->sum('age'), 65, 'the sum of all ages is now 65'); is($aggr_query_count, 1, 'sum triggered one query'); } # Now ensure that changes to members are reflected in the set. { my $cool_person_count = $cool_person_set->count; my $jamesbond = URT::Person->create(name => 'James Bond', is_cool => 1, age => '35'); ok($jamesbond, 'Create a new cool person'); $aggr_query_count = 0; is($cool_person_set->count, $cool_person_count + 1, 'count increased'); is($aggr_query_count, 0, 'count did not trigger query'); my $fred = URT::Person->get(12); is($fred->is_cool, 0, 'fred is not cool (yet)'); $fred->is_cool(1); $aggr_query_count = 0; is($cool_person_set->count, $cool_person_count + 2, 'count increased again'); is($aggr_query_count, 0, 'count did not trigger query'); $aggr_query_count = 0; ok($jamesbond->delete, 'Delete James Bond'); is($cool_person_set->count, $cool_person_count + 1, 'count decreased after delete'); is($aggr_query_count, 0, 'Made no queries'); } 18_indirect_accessor.t000444023532023421 475412121654173 17172 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 19; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); UR::Object::Type->define( class_name => "Acme::Boss", has => [ id => { type => "Number" }, name => { type => "String" }, company => { type => "String" }, ] ); UR::Object::Type->define( class_name => 'Acme::Employee', has => [ name => { type => "String" }, boss => { type => "Acme::Boss", id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'name' }, company => { via => 'boss' }, ] ); my $b1 = Acme::Boss->create(name => "Bosser", company => "Some Co."); ok($b1, "created a boss object"); my $e1 = Acme::Employee->create(boss => $b1); ok($e1, "created an employee object"); ok($e1->can("boss_name"), "employees can check their boss' name"); ok($e1->can("company"), "employees can check their boss' company"); is($e1->boss_name,$b1->name, "boss_name check works"); is($e1->company,$b1->company, "company check works"); $b1->name("Crabber"); $b1->company("Other Co."); is($e1->boss_name,$b1->name, "boss_name check works again"); is($e1->company,$b1->company, "company check still works"); my $b2 = Acme::Boss->create(name => "Chief", company => "Yet Another Co."); ok($b2, "made another boss"); $e1->boss($b2); is($e1->boss,$b2, "re-assigned the employee to a new boss"); is($e1->boss_name,$b2->name, "boss_name check works"); is($e1->company,$b2->company, "company check works"); # Hmmm... this only triggered the bug on DataSources backed by a real database my @matches = Acme::Employee->get(boss => 'nonsensical'); ok(scalar(@matches) == 0, 'get employees by boss without boss objects correctly returns 0 items'); my $e2 = Acme::Employee->create(name => 'Bob', boss_name => 'Chief'); ok($e2, 'created an employee via a boss_name that already exists'); is($e2->boss_id, $b2->id, 'boss_id of new employee is correct, did not make a new Acme::Boss'); my %existing_boss_ids = map { $_->id => $_ } Acme::Boss->get(); my $e3 = Acme::Employee->create(name => 'Freddy', boss_name => 'New Boss'); ok($e3, 'Created an employee via a boss_name that did not previously exist'); ok($e3->boss_id, 'it has a boss_id'); ok($e3->boss, 'it has a boss object'); ok(! exists $existing_boss_ids{$e3->boss_id}, 'The new boss_id did not exist before creating this employee'); 89_loading_with_boolexpr_evaluate.t000444023532023421 460612121654173 21763 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); my $query_count; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $query_count = 0; # Since this query is against a calculate property, it will do a more # general query, import more data than is strictly necessary, and throw # out objects after loading them because they don't pass BoolExpr evaluation my @things = URT::Person->get(uc_name => 'lowercase'); is(scalar(@things), 0, 'No Persons with uc_name => "lowercase"'); is($query_count, 1, 'Made 1 query'); # This will actually trigger another DB query, though all the objects # it loads will already exist in the context. The underlying context # iterator needs to correctly throw away non-matching objects and # only return the one we're looking for $query_count = 0; @things = URT::Person->get(uc_name => 'FRED'); is(scalar(@things), 1, 'Got 1 thing with uc(name) FRED'); is($things[0]->name, 'Fred', 'Name is correct'); is($query_count, 1, 'Made 1 query'); sub create_db_tables { my $dbh = shift; ok($dbh->do('create table person ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created things table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ 'person_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'uc_name' => { calculate_from => 'name', calculate => 'uc($name)' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Person"); ok($dbh->do(q(insert into person (person_id, name) values (1, 'Bob'))), 'insert a person'); ok($dbh->do(q(insert into person (person_id, name) values (2, 'Joe'))), 'insert a person'); ok($dbh->do(q(insert into person (person_id, name) values (3, 'Fred'))), 'insert a person'); } 93b_namespace_loaded_from_symlink.t000444023532023421 167412121654173 21707 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 7; use Cwd; use File::Temp; use IO::File; # Test the condition when one of the directories in @INC is a symlink, load a Namespace # module from that directory, and make sure the entry in @INC and %INC have turned that # path into an absolute path my $temp_dir = File::Temp::tempdir(CLEANUP => 1); ok($temp_dir, 'Create temp directory to hold symlink'); my $dir = Cwd::abs_path(File::Basename::dirname(__FILE__) . '/../../'); ok(-f $dir.'/Slimspace.pm', 'Found Slimspace.pm'); my $inc_dir = $temp_dir .'/inc'; ok(symlink($dir, $inc_dir), 'Create symlink'); unshift @INC, $inc_dir; is($INC[0], $inc_dir, 'First in \@INC is the temp dir synlink'); use_ok('Slimspace'); my $path = $INC{'Slimspace.pm'}; my $abs_path = Cwd::abs_path($path); is($path, $abs_path, '\%INC for Slimspace.pm is the absolute path'); is($INC[0], $dir, 'First in \@INC was rewritten to be absolute path'); 22_cached_get_with_subclasses.t000444023532023421 447712121654173 21034 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 21; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; # FIXME - make another test that does something similar but the items are in the DB UR::Object::Type->define( class_name => 'Acme::Person', id_by => ['person_id'], has => ['name'], ); UR::Object::Type->define( class_name => 'Acme::Employee', is => 'Acme::Person', has => [ 'title' ], ); UR::Object::Type->define( class_name => 'Acme::Customer', is => 'Acme::Person', has => [ 'address' ], ); { my $p1 = Acme::Employee->create(person_id => 1, name => 'Bob', title => 'worker'); ok($p1, 'Created employee 1'); ok($p1->isa('Acme::Employee'), 'Employee 1 isa Acme::Employee'); ok($p1->isa('Acme::Person'), 'Employee 1 isa Acme::Person'); ok(! $p1->isa('Acme::Customer'), 'Employee 1 is not a Acme::Customer'); } { my $p2 = Acme::Employee->create(person_id => 2, name => 'Fred', title => 'boss'); ok($p2, 'Created employee 2'); ok($p2->isa('Acme::Employee'), 'Employee 2 isa Acme::Employee'); ok($p2->isa('Acme::Person'), 'Employee 2 isa Acme::Person'); ok(! $p2->isa('Acme::Customer'), 'Employee 2 is not a Acme::Customer'); } { my $p3 = Acme::Customer->create(person_id => 3, name => 'Joe', address => '123 Main St'); ok($p3, 'Created customer'); ok(! $p3->isa('Acme::Employee'), 'Customer is not a Acme::Employee'); ok($p3->isa('Acme::Person'), 'Customer isa Acme::Person'); ok($p3->isa('Acme::Customer'), 'Customer isa Acme::Customer'); } { my $p = Acme::Customer->get(person_id => 3); ok($p, 'Got a Person with the subclass by id'); ok($p->isa('Acme::Person'), 'It is a Acme::Person'); ok($p->isa('Acme::Customer'), 'It is a Acme::Customer'); ok(! $p->isa('Acme::Employee'), 'It is not a Acme::Employee'); } { my $p = Acme::Person->get(person_id => 3); ok($p, 'Got a Person with the base class by id'); ok($p->isa('Acme::Person'), 'It is a Acme::Person'); ok($p->isa('Acme::Customer'), 'It is a Acme::Customer'); ok(! $p->isa('Acme::Employee'), 'It is not a Acme::Employee'); } { my $p = Acme::Employee->get(person_id => 3); is($p, undef, 'Getting an employee with the id of a customer correctly returns nothing'); } 61_iterator_merge_changed_objs_with_db.t000444023532023421 1176412121654173 22722 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 18; my $dbh = &setup_classes_and_db(); # This tests creating an iterator and doing a regular get() # for the same stuff, and make sure they return the same things # create the iterator but don't read anything from it yet my $iter = URT::Thing->create_iterator(name => 'Bob'); ok($iter, 'Created iterator for Things named Bob'); my $o = URT::Thing->get(thing_id => 2); my @objs = URT::Thing->get(name => 'Bob'); is(scalar(@objs), 2, 'Get returned 2 objects'); my @objs_iter; while (my $obj = $iter->next()) { push @objs_iter, $obj; } is(scalar(@objs_iter), 2, 'The iterator returned 2 objects'); is_deeply(\@objs_iter, \@objs, 'Iterator and get() returned the same things'); # Iterator behavior is undefined when the caller manipulates the objects # matching the iterator's BoolExpr after the iterator's creation, but before # they come off of the iterator. # # In this case, the iterator will only return the one object still matching # the bx when it's next() method is called, but not the thing that didn't # exist when the iterator was created. # Right now objects 6,8 and 10 are named Joe $iter = URT::Thing->create_iterator(name => 'Joe'); ok($iter, 'Created iterator for Things named Joe'); $o = URT::Thing->get(thing_id => 6); $o->name('Fred'); # Change the name so it no longer matches the request $o = URT::Thing->get(thing_id => 10); $o->delete(); # Delete this one @objs = URT::Thing->get(name => 'Joe'); is(scalar(@objs), 1, 'get() returned 1 thing named Joe after changing the other'); ok(URT::Thing->create(thing_id => 99, name => 'JoeJoe', data => 'abc'), 'Make a new thing that matches the iterator BoolExpr'); $o = $iter->next(); is($o->id, 8, 'Second object from iterator is id 8'); is($o->name, 'Joe', 'Second object name is Joe'); $o = $iter->next(); ok(!$o, 'The iterator is done'); # doesn't return the newly created thing # Make an iterator ordered by 'data', and change 'data' for some of the objects # while it's running. # # Note for future developers: The behavior here is a policy decision, not really # a logical or technological one. If the behavior changes in the future, that # might be ok, but it would need to be documented # initially, the order is 99 (abc), 8 (bar), 4 (baz), 2 (foo), 6 (foo) $iter = URT::Thing->create_iterator('id <' => 100, -order => 'data'); ok($iter, 'Create iterator for all things ordered by data'); # The DB query won't see this because the cursor was opened before the update ok($dbh->do("update things set data = 'aaa' where thing_id = 2"), 'Change data to "aaa" for thing 2 in the DB, it now sorts first'); my @objects; # This should fill in 99 and 8 @objects = ($iter->next(), $iter->next()); ok(URT::Thing->get(4)->delete, 'Delete thing id 4 before the iterator returns it'); $o = eval { $iter->next() }; like($@, qr/Attempt to fetch an object which matched.*'thing_id' => ('|)4('|)/s, 'caught exception about deleted thing id 4'); # completely-consistent iterator behaviour would make this one come next URT::Thing->get(6)->data('bas'); # And might make this one come again at the end of the list URT::Thing->get(99)->data('zzz'); push @objects, $o while ($o = $iter->next()); my @expected_ids = (99,8,2,6); my @got_ids = map { $_->id } @objects; is_deeply(\@got_ids, \@expected_ids, 'Objects are in the expected order'); # Remove the test DB unlink(URT::DataSource::SomeSQLite->server); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'], [6, 'Joe', 'foo'], [8, 'Joe', 'bar'], [10, 'Joe','baz'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); return $dbh; } 24_query_by_is_transient.t000444023532023421 455312121654173 20122 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_class varchar)'), 'created product table'); ok($dbh->do('create table cool_product ( product_id int NOT NULL PRIMARY KEY, coolness integer )'), 'created cool_product table'); ok($dbh->do("insert into product values (1,'race car', 'URT::Product::Cool')"), 'insert row into product for race car'); ok($dbh->do("insert into cool_product values (1,10)"), 'insert row into cool_product for race car'); ok($dbh->do("insert into product values (2,'pencil','URT::Product::NotCool')"), 'insert row into product for pencil'); UR::Object::Type->define( class_name => 'URT::Product', is_abstract => 1, subclassify_by => 'product_class', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_class => { is => 'Text' }, coolness => { is_abstract => 1, is_transient => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); UR::Object::Type->define( class_name => 'URT::Product::Cool', is => 'URT::Product', id_by => 'product_id', has => [ coolness => { is => 'Number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'cool_product', ); UR::Object::Type->define( class_name => 'URT::Product::NotCool', is => 'URT::Product', id_by => 'product_id', has_constant => [ coolness => { is => 'Number', is_classwide => 1, value => 0 }, ], ); my @p = URT::Product->get('coolness >' => 0); is(scalar(@p), 1, 'Got one product with positive coolness'); isa_ok($p[0], 'URT::Product::Cool'); is($p[0]->product_name, 'race car', 'name is correct'); @p = URT::Product->get(coolness => 0); is(scalar(@p), 1, 'Got one product with zero coolness'); isa_ok($p[0], 'URT::Product::NotCool'); is($p[0]->product_name, 'pencil', 'name is correct'); @p = URT::Product->get('product_name true' => 1, -hints => ['coolness']); is(scalar(@p), 2, 'Getting products with -hints => coolness got 2 items'); 78b_get_by_subclass_property.t000444023532023421 460312121654173 20764 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 10; # This tests a get() by subclass specific parameters on a subclass with no table of its own. # The property is only defined on the subclass, but the data lives in the table referred to # in the parent class my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do(q{ create table animal ( animal_id integer NOT NULL, name varchar NOT NULL, num_legs integer, subclass varchar NOT NULL)}), 'Created animal table'); ok($dbh->do("insert into animal (animal_id, name, subclass, num_legs) values (1,'fido','URT::Dog', 4)"), 'Inserted fido'); ok($dbh->do("insert into animal (animal_id, name, subclass, num_legs) values (2,'woody','URT::Bird', 2)"), 'Inserted woody'); ok($dbh->do("insert into animal (animal_id, name, subclass) values (3,'jaws','URT::Shark')"), 'Inserted jaws'); ok($dbh->commit(), 'DB commit'); # Dogs and birds have legs, sharks don't UR::Object::Type->define( class_name => 'URT::Animal', id_by => [ animal_id => { is => 'NUMBER', len => 10 }, ], has => [ name => { is => 'Text' }, subclass => { is => 'Text' }, ], subclassify_by => 'subclass', data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Dog', is => 'URT::Animal', has_optional => [ num_legs => { is => 'Integer' }, ], ); UR::Object::Type->define( class_name => 'URT::Bird', is => 'URT::Animal', has_optional => [ num_legs => { is => 'Integer', column_name => 'num_legs' }, ], ); UR::Object::Type->define( class_name => 'URT::Shark', is => 'URT::Animal', ); my @animals = URT::Dog->get(num_legs => 3); is(scalar(@animals), 0, 'No dogs with 3 legs'); @animals = URT::Bird->get(num_legs => 2); is(scalar(@animals), 1, 'Got 1 bird with 2 legs'); is($animals[0]->name, 'woody', ' It was the right animal'); @animals = eval { URT::Animal->get(num_legs => 0) }; like($@, qr/Unknown parameters to URT::Animal get()/, 'Correctly got an exception trying to query URT::Animal by num_legs'); 06_accessor_simple.t000444023532023421 133312121654173 16645 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my ($obj,$same_obj); use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name/] ); $obj = Acme::Product->create(name => "dynamite", manufacturer_name => "Explosives R US"); ok($obj, 'Created object with name and manufacturer_name'); is($obj->name, "dynamite", 'name accessor works'); is($obj->manufacturer_name, "Explosives R US", 'manufacturer_name accessor works'); # $same_obj = Acme::Product->get(name => "dynamite"); is($obj,$same_obj, 'Get same object returns the same reference'); 94_chain_join.t000444023532023421 1356312121654173 15632 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # the initial code is from test 91b, to set-up some joinable data use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table CAR_ENGINE (engine_id int NOT NULL PRIMARY KEY, car_id integer references CAR(car_id), size number)'), 'created car_engine table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1], is_optional => 1 }, # direct where big_cars => { is => 'URT::Car', via => 'cars', to => '__self__', where => [ 'engine_size >=' => 400 ], }, # indirect where car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, engine_size => { is => 'Number', via => 'engine', to => 'size' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car_engine values (?,?,?)'); foreach my $row ( [100, 1, 350], [ 200, 2, 400], [300, 3, 428], [400, 4, 429], [500, 5, 289] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); #$DB::single = 1; # chain property equiv my $bx1 = URT::Person->define_boolexpr('primary_car.color' => 'red'); ok($bx1, "got bx with property chain"); my @p1 = URT::Person->get('primary_car.color' => 'red'); is(scalar(@p1), 1, "got one person with a primary car color of red using a property chain"); my @p2 = URT::Person->get('primary_car_color' => 'red'); is(scalar(@p2),1,"got one person with a primary car color of red using a custom accessor"); is($p1[0], $p2[0], "result matches"); my @p3 = URT::Person->get('primary_car.color' => ['red']); is(scalar(@p3), 1, "got one person with a primary car color of red using a property chain and the \"in\" operator"); my $bx5 = URT::Person->define_boolexpr('cars.color' => 'blue', 'cars.engine.size' => '400'); #print "$bx5"; #$ENV{UR_DBI_MONITOR_SQL} = 1; $DB::single = 1; my @p5 = URT::Person->get($bx5); ok("@p5", "regular query works for " . scalar(@p5) . " objects"); __END__ my $bx4i = URT::Person->define_boolexpr('big_cars.color' => 'red'); my $bx4f = $bx4i->flatten; print "$bx4i\n$bx4f\n"; my @p4f = URT::Person->get($bx4f); ok("@p4f", "flat query $bx4f works for " . scalar(@p4f) . " objects"); # we must flatten before query for this to work, and currently constant_values need support my @p4i = URT::Person->get($bx4i); ok("@p4i", "indirect query works"); is("@p4i", "@p4f", "indirect and flat query results match"); # the bx "operator" could be named "subquery" or we turn "matches" into "matches-bx" and "matches-regex" #my @p = URT::Person->get('primary_car.color' => 'red'); my $rule1 = URT::Car->define_boolexpr(color => 'red'); ok($rule1, "made a 'car has color red' rule"); note("$rule1"); #$DB::single = 1; my $rule2 = URT::Person->define_boolexpr('cars bx' => $rule1->id); ok($rule2, "made a 'person has primary_car with color is red'"); note("$rule2"); my @p = URT::Person->get($rule2); is(scalar(@p), 1, "got one person with a red primary car"); is($p[0]->id, 13, "got the expected person"); 67_composite_id_with_id_class_by_rt55121.t000444023532023421 201112121654173 22642 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 4; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; UR::Object::Type->define( class_name => 'Acme::Composited::Polygon', id_by => [ qw/size color shape/ ] ); UR::Object::Type->define( class_name => 'Acme::Box', has_abstract_constant => [ 'subject_class_name' ], has => [ subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id', doc => 'the object being boxed' } ] ); my ($obj,$box); $obj = Acme::Composited::Polygon->create( size => 'big', color => 'blue', shape => 'square' ); ok($obj,'make the composited id object'); $box = Acme::Box->create( subject_class_name => 'Acme::Composited::Polygon' ); ok($box,'make the container'); ok($box->subject($obj),'set subject on container'); ok($box->subject,'container still has subject'); 31_ref_as_value.t000444023532023421 1135512121654173 16150 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use URT::ObjWithHash; use strict; use warnings; plan tests => 27; my $o = URT::ObjWithHash->create(myhash1 => { aaa => 111, bbb => 222 }, mylist => [ ccc => 333, ddd => 444 ]); my @h = ($o->myhash1, $o->mylist); #diag "data was: " . Data::Dumper::Dumper($o,@h); is(ref($h[0]),'HASH', "got a hashref back"); is(ref($h[1]),'ARRAY', "got an arrayref back"); is_deeply($h[0],{ aaa => 111, bbb => 222 },"got correct values back for hashref"); #TODO: { # local $TODO = 'array seems to be out of order'; is_deeply($h[1],[ ccc => 333, ddd => 444 ],"got correct values back for arrayref"); #}; # make sure things being associated with objects # are not being copied in the constructor class TestClassB { has => [ value => { is => 'String' }, ], }; class TestClassA { has => [ b_thing => { is => 'TestClassB' } ], }; my $ax = TestClassA->create(); ok($ax, "Created TestClassA without b_thing"); my $bx = TestClassB->create( value => 'abcdfeg' ); ok($bx, "Created TestClassB with value"); ok($ax->b_thing($bx), "Set b_thing to TestClassB object"); is($ax->b_thing, $bx, "b_thing is TestClassB object"); my $ay = TestClassA->create( b_thing => $bx ); ok($ay, "Created TestClassA with bx as b_thing"); is($ax->b_thing,$ay->b_thing, "ax->b_thing is ay->b_thing"); ok($bx->value('oyoyoy'), "Changed bx->value"); is($ax->b_thing->value, $ay->b_thing->value, "ax->b_thing value is ay->b_thing value"); my $by = TestClassB->create( value => 'zzzykk' ); ok($by, "Created TestClassB with value"); ok($ay->b_thing($by), "Changed ay b_thing to by"); isnt($ax->b_thing,$ay->b_thing,"ax b_thing is not ay b_thing"); isnt($ax->b_thing->value,$ay->b_thing->value,"ax->b_thing value is not ay->b_thing value"); class TestClassC { has => [ foo => { is => 'ARRAY' } ] }; my $c; ok($c = TestClassC->create,"Created TestClassC with no properties"); ok($c->foo([qw{foo bar baz}]),"Set foo"); is_deeply($c->foo,[qw{foo bar baz}],'Checking array'); ok($c = TestClassC->create( foo => [qw{foo bar baz}] ),"Created TestClassC with foo arrayref"); is_deeply($c->foo,[qw{foo bar baz}],'Checking array for alpha-sort'); #TODO: { # local $TODO = 'somewhere, somehow PAP workflow does this.... so lets make sure it works'; my $d; ok(eval { $d = TestClassC->create( foo => [ $c, ## first element is a ur object [ ## next is a psuedo hash, or something that looks like one { make => 1, perl => 2, mad => 3, at => 4, us => 5 }, 'this', 'is', 'a', 'pseudo', 'hash' ] ] ) }, "created TestClassC with psuedo-hash like array"); # diag "data was: " . Data::Dumper::Dumper($d); #} # make Bar a real class so it is not mistaken for a primitive package Bar; sub bar {}; package main; # new rule logic seems to allow boolexpr references to be cloned for my $c ('Bar') { class Foo { has => [ a => { is => $c }, b => { is => $c }, c => { is => $c } ] }; my @r = map { bless({ id => $_ },$c); } (100..102); my @f = qw/a b c/; my $oo = Foo->define_boolexpr(a => $r[0], c => $r[2], b => $r[1]); my %pp = $oo->params_list; my @pp = @pp{@f}; my $o = $oo->normalize; my %p = $o->params_list; my @p = @p{@f}; my $str = Data::Dumper::Dumper(\@r,\%pp,\%p,$oo,$o); is("@pp", "@r", "unnormalized rule decomposes correctly") or diag $str; is("@p", "@r", "normalized rule decomposes correctly") or diag $str; } my @p = ( 'myhash1', { 'bbb' => 222, 'aaa' => 111 }, 'mylist', [ 'ccc', 333, 'ddd', 444 ], 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', 'id', 'linus43.gsc.wustl.edu 21757 1286150139 10001', ); my $b = URT::ObjWithHash->define_boolexpr(@p); my $hu = $b->value_for('myhash1'); my $au = $b->value_for('mylist'); note($hu); note($au); my $n = $b->normalize; my $hn = $n->value_for('myhash1'); my $an = $n->value_for('mylist'); is($an,$au,"the normalized array is the same ref as the unnormalized"); is($hn,$hu,"the normalized array is the same ref as the unnormalized"); my %b = $b->params_list; my %n = $n->params_list; my @b = map { $_ => $b{$_}.'' } sort keys(%b); my @n = map { $_ => $n{$_}.'' } sort keys(%n); is("@n","@b", "normalization keeps references correct"); 87_get_by_different_params_updates_query_cache.t000444023532023421 422712121654173 24447 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; use URT::DataSource::SomeSQLite; # Get an object into memory with a query. Re-get it with a second query (which will hit the DB # again because it doesn't know it doesn't really have to). Finally, do the second query again # and it should not hit the DB my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); # Insert some data # Bob, Joe and Frank are cool # Fred and Mike are not my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(name => ['Bob','Joe','Frank']); is(scalar(@p), 3, 'Got 3 people with an in-clause'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @p = URT::Person->get(is_cool => 1); is(scalar(@p), 3, 'Got the same 3 people with a different query'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @p = URT::Person->get(is_cool => 1); is(scalar(@p), 3, 'Got the same 3 people with the second query again'); is_deeply([map { $_->id } @p], [1,4,5], 'Got the right people'); is($query_count, 0, 'Made 1 query'); 80b_namespace_command_base.t000444023532023421 1206612121654173 20307 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use Cwd; use File::Temp; my $test_directory = Cwd::abs_path(File::Basename::dirname(__FILE__)); my $original_cwd = Cwd::getcwd(); my $temp_dir = File::Temp::tempdir(CLEANUP => 1); ok(UR::Object::Type->define( class_name => 'URT::Command::TestBase', is => 'UR::Namespace::Command::Base', ), 'Define test command class'); URT::Command::TestBase->dump_error_messages(0); URT::Command::TestBase->queue_error_messages(1); chdir ($temp_dir) || die "Can't chdir to $temp_dir: $!"; my $namespace_name = URT::Command::TestBase->resolve_namespace_name_from_cwd(); ok(!defined($namespace_name), 'resolve_namespace_name_from_cwd returns nothing when not in a namespace directory'); my $cmd = URT::Command::TestBase->create(); ok(!$cmd, 'Cannot create command when pwd is not inside a namespace dir'); my $error_message = join("\n",URT::Command::TestBase->error_messages()); like($error_message, qr(Could not determine namespace name), 'Error message was correct'); my $lib_path = URT::Command::TestBase->resolve_lib_path_for_namespace_name('URT'); my($expected_lib_path) = ($test_directory =~ m/^(.*)\/URT\/t/); is($lib_path, $expected_lib_path, 'resolve_lib_path_for_namespace_name found the URT namespace'); $cmd = URT::Command::TestBase->create(namespace_name => 'URT'); ok($cmd, 'Created command in a temp dir with forced namespace_name'); is($cmd->namespace_name, 'URT', 'namespace_name is correct'); $expected_lib_path = $INC{'URT.pm'}; $expected_lib_path =~ s/\/URT.pm$//; is($cmd->lib_path, $expected_lib_path, 'lib_path is correct'); chdir($test_directory) || die "Can't chdir to $test_directory: $!"; $cmd = URT::Command::TestBase->create(); ok($cmd, 'Created command in the URT test dir and did not force namespace_name'); $lib_path = $cmd->lib_path; is($lib_path, $expected_lib_path, 'lib_path is correct'); chdir($lib_path) || die "Can't chdir to $lib_path: $!"; is($cmd->working_subdir, '.', 'when pwd is lib_path, working_subdir is correct'); chdir($test_directory) || die "Can't chdir to $test_directory"; is($cmd->working_subdir, 'URT/t', 'When pwd is the test directory, working_subdir is correct'); chdir($temp_dir) || die "Can't chdir to $temp_dir: $!"; my $expected_working_subdir = $lib_path . ('../' x scalar(my @l = split('/', Cwd::abs_path($lib_path)))) . $temp_dir; #is($cmd->working_subdir, $expected_working_subdir, 'when pwd is somwehere in /tmp, working_subdir is correct'); chdir($original_cwd); my $expected_namespace_path = $INC{'URT.pm'}; $expected_namespace_path =~ s/\.pm$//; is($cmd->namespace_path, $expected_namespace_path, 'namespace_path is correct'); is($cmd->command_name, 'u-r-t test-base', 'command_name is correct'); # This needs to be updated if we ever drop in a new module under URT/ my @expected_modules = sort qw(URT/34Baseclass.pm URT/38Primary.pm URT/43Primary.pm URT/ObjWithHash.pm URT/Thingy.pm URT/34Subclass.pm URT/38Related.pm URT/43Related.pm URT/RAMThingy.pm URT/Vocabulary.pm URT/Context/Testing.pm URT/DataSource/CircFk.pm URT/DataSource/Meta.pm URT/DataSource/SomeFile.pm URT/DataSource/SomeFileMux.pm URT/DataSource/SomeMySQL.pm URT/DataSource/SomeOracle.pm URT/DataSource/SomePostgreSQL.pm URT/DataSource/SomeSQLite.pm); my @modules = sort $cmd->_modules_in_tree(); # remove modules created by the 'ur update classes-from-db' test that may be running in parallel @modules = grep { $_ !~ m/Car.pm|Person.pm|Employee.pm/ } @modules; is_deeply(\@modules, \@expected_modules, '_modules_in_tree with no args is correct'); my @expected_class_names = sort qw(URT::34Baseclass URT::38Primary URT::43Primary URT::ObjWithHash URT::Thingy URT::34Subclass URT::38Related URT::43Related URT::RAMThingy URT::Vocabulary URT::Context::Testing URT::DataSource::CircFk URT::DataSource::Meta URT::DataSource::SomeFile URT::DataSource::SomeFileMux URT::DataSource::SomeMySQL URT::DataSource::SomeOracle URT::DataSource::SomePostgreSQL URT::DataSource::SomeSQLite); my @class_names = sort $cmd->_class_names_in_tree; # remove classes created by the 'ur update classes-from-db' test that may be running in parallel @class_names = grep { $_ !~ m/URT::Car|URT::Person|URT::Employee/ } @class_names; is_deeply(\@class_names, \@expected_class_names, '_class_names_in_tree with no args is correct'); @modules = sort $cmd->_modules_in_tree( qw( URT/34Baseclass.pm URT/DataSource/Meta.pm URT/Something/NonExistent.pm URT::34Baseclass URT::DataSource::SomeOracle URT::NotAModule ) ); @expected_modules = sort qw( URT/34Baseclass.pm URT/DataSource/Meta.pm URT/34Baseclass.pm URT/DataSource/SomeOracle.pm ); is_deeply(\@modules, \@expected_modules, '_modules_in_tree with args is correct'); 04b_mysql.t000444023532023421 62612121654173 14763 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring MySQL"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $dbh = URT::DataSource::SomeMySQL->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 82_boolexpr_op_underscore.t000555023532023421 66512121654173 20251 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use UR; class Foo { has => [ _bar => {}, ], }; my $bx1 = Foo->define_boolexpr( _bar => { operator => '!=', value => undef}); my $bx2 = Foo->define_boolexpr( '_bar !=' => undef); is( $bx2->id, $bx1->id, "Boolean expression created with an operator, with an operator using the new syntax and using a parameter name with an underbar works."); 85_method_meta.t000444023532023421 44412121654174 15752 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More skip_all => 'under development'; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; package Foo; class Foo { }; package main; isa_ok('Foo',"UR::Object"); 21_observer.t000444023532023421 1255012121654174 15342 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 40; UR::Object::Type->define( class_name => 'URT::Parent', is_abstract => 1, valid_signals => ['something_else'], ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::Parent', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], ); my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $change_count = get_change_count(); my $observations = {}; $p1->last_name("DoDo"); is_deeply($observations, {}, "no callback count change with no observers defined"); is(get_change_count(), $change_count + 1, '1 change recorded even with no observers'); foreach my $thing ( $p1,$p2,'URT::Person','URT::Parent') { foreach my $aspect ( '','last_name','something_else' ) { my $id = ref($thing) ? $thing->id : $thing; my %args = ( callback => sub { no warnings 'uninitialized'; $observations->{$id}->{$aspect}++ } ); if ($aspect) { $args{'aspect'} = $aspect; } ok($thing->add_observer(%args), "Made an observer on $thing for aspect $aspect"); } } $change_count = get_change_count(); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply($observations, { 1 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Do"),"Do", "changed person 2"); is_deeply($observations, { 2 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; ok($p2->__signal_observers__('something_else'),'send the "something_else" signal to person 2'); is_deeply($observations, { 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_observers__('something_else'), 'Send the "something_else" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1, 'something_else' => 1}, 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; # Signals don't propagate down the inheritance tree, only up ok(URT::Parent->__signal_observers__('something_else'), 'Send the "something_else" signal to the URT::Parent class'); is_deeply($observations, { 'URT::Parent' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_observers__('blablah'), 'Send the "blahblah" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1,}, 2 => { '' => 1,}, 'URT::Person' => { '' => 1,}, 'URT::Parent' => { '' => 1,}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); ok(scalar($p1->remove_observers()), 'Remove observers for Person 1'); $change_count = get_change_count(); $observations = {}; is($p1->last_name("Doooo"),"Doooo", "changed person 1"); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 } }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Boo"),"Boo", "changed person 2"); is_deeply($observations, { 'URT::Person' => { '' => 1, 'last_name' => 1 }, 'URT::Parent' => { '' => 1, 'last_name' => 1 }, 2 => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); sub get_change_count { my @c = map { scalar($_->__changes__) } URT::Person->get; my $sum = 0; do {$sum += $_ } foreach (@c); return $sum; } 49d_complicated_get_joining_through_view.t000444023532023421 1134212121654174 23321 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table # - All the "tables" involved are areally inline views &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_is_smart => 1); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL, do_include integer)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL, do_include integer)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id), do_include integer)"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id), do_include integer)"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name, do_include) values (?,?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart, do_include) values (?,?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id, do_include) values (?,?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id, do_include) values (?,?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie',1); $breed_insert->execute(1,1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie',1); $animal_insert->execute(2, 1,1); # a person named Jeff $name_insert->execute(3, 'Jeff',1); $person_insert->execute(3,2,1); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from named_thing where do_include = 1) named_thing_view', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from breed where do_include = 1) breed_view', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from animal where do_include = 1) animal_view', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => '(select * from person where do_include = 1) person_view', ); } 20a_has_many_with_multiple_ids.t000444023532023421 524012121654174 21235 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT; use Test::More tests => 11; UR::Object::Type->define( class_name => 'URT::Order', table_name => 'orders', id_by => [ order_id => { is => 'integer', is_optional => 1, column_name => 'order_id' }, ], has_many => [ attributes => { is => 'URT::OrderAttribute', reverse_as => 'order' }, tracking_number => { is => 'String', via => 'attributes', to => 'value', where => [key => 'tracking_number'], is_mutable => 1}, ship_date => { is => 'String', via => 'attributes', to => 'value', where => [key => 'ship_date'], is_mutable => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::OrderAttribute', id_by => [ order => { is => 'URT::Order', id_by => 'order_id' }, key => { is => 'String' }, value => { is => 'String' }, ], table_name => 'order_attributes', data_source => 'URT::DataSource::SomeSQLite', ); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do("create table orders (order_id integer NOT NULL PRIMARY KEY)"); $dbh->do("create table order_attributes ( order_id integer NOT NULL references orders(order_id), key varchar NOT NULL, value varchar NOT NULL, PRIMARY KEY(order_id, key,value))"); $dbh->do("insert into orders values (99)"); $dbh->do("insert into order_attributes values (99,'tracking_number','abc123')"); $dbh->do("insert into order_attributes values (99,'ship_date','2011 Jan 1')"); my $o = URT::Order->get(99); ok($o, 'Retrieved an order'); is($o->tracking_number, 'abc123', 'tracking_number attribute is OK'); is($o->ship_date, '2011 Jan 1', 'ship_date attribute is OK'); $o = URT::Order->create(id => 1); ok($o, "order object created"); ok($o->add_attribute(key => 'tracking_number', value => 'xyzzy'), 'Added tracking number attribute'); ok($o->add_ship_date('2011 Jan 7'), 'Added ship date'); ok(UR::Context->commit(), 'Commit'); my $rows = $dbh->selectrow_arrayref('select * from orders where order_id = 1'); ok($rows, 'Got row for order 1 from DB'); is($rows->[0], 1,'order_id is correct'); $rows = $dbh->selectall_arrayref('select * from order_attributes where order_id = 1 order by key'); ok($rows, 'Got attributes for order_id 1'); my $expected = [ [1,'ship_date','2011 Jan 7'], [1,'tracking_number','xyzzy']]; is_deeply($rows, $expected, 'Attribute data is ok'); 49h_complicated_get_double_join.t000444023532023421 625312121654174 21354 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 2; # Similar to the other double-join test. The same table gets joined in and needs a different filter # for each join. # # This test is different in that there is an additional join between the two person objects, and # the property names end up sorting in different orders between test 1 and 2 my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL)'); $dbh->do('create table attribute (attr_id integer PRIMARY KEY NOT NULL, person_id integer NOT NULL REFERENCES person(person_id), name varchar NOT NULL, value varchar)'); $dbh->do('create table relationship (person_id integer REFERENCES person(person_id), related_person_id integer REFERENCES person(person_id), name varchar NOT NULL, PRIMARY KEY (person_id, related_person_id))'); # Make 2 people named Bob and Fred, they are siblings $dbh->do("insert into person values (1)"); $dbh->do("insert into attribute values (1,1,'name', 'Bob')"); $dbh->do("insert into person values (2)"); $dbh->do("insert into attribute values (3,2,'name', 'Fred')"); $dbh->do("insert into relationship values (1,2,'sibling')"); $dbh->do("insert into relationship values (2,1,'sibling')"); UR::Object::Type->define( class_name => 'Person', table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has_many => [ attributes => { is => 'Attribute', reverse_as => 'person' }, relationships => { is => 'Relationship', reverse_as => 'person' }, ], has => [ name => { via => 'attributes', to => 'value', where => [name => 'name']}, zname => { via => 'attributes', to => 'value', where => [name => 'name']}, sibling => { is => 'Person', via => 'relationships', to => 'related_person', where => [name => 'sibling'] }, sibling_name => { via => 'sibling', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Attribute', table_name => 'attribute', data_source => 'URT::DataSource::SomeSQLite', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, name => { is => 'String' }, value => { is => 'String' }, ], ); UR::Object::Type->define( class_name => 'Relationship', table_name => 'relationship', data_source => 'URT::DataSource::SomeSQLite', id_by => [ person_id => { is => 'Integer' }, related_person_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id'}, related_person => { is => 'Person', id_by => 'related_person_id' }, name => { is => 'String' }, ], ); my @p = Person->get(name => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person joining name before sibling'); @p = Person->get(zname => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person joining name after sibling'); 16_viewer.t000444023532023421 352112121654174 14776 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More; eval { App::UI->user_interface("gtk2") }; if ($@) { plan skip_all => "skipping because gtk will not initialize", } else { plan tests => 7; #Gtk->timeout_add( if(0) { Glib::Timeout->add( 1000, # = 1 second ...slow me down for debugging sub { my @w = App::UI::Gtk2->windows(); for my $window (@w) { my $viewer_widget = $window->child(); $window->remove($viewer_widget); $window->destroy; App::UI::Gtk2->remove_window($window); } return 1; } ); } } App->init; my $v = URT::RAMThingy->create_viewer( toolkit => "gtk2", aspects => [ 'clone_name', 'chromosome', ], ); ok($v, "created a viewer"); $v->show_modal; my @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name chromosome/], "aspects are correct"); my $s = URT::RAMThingy->create(clone_name => "MY_CC-loneA01", map_order => 123, chromosome => "y"); ok($s, "created a subject"); $v->set_subject($s); is($v->get_subject,$s, "set the subject for the viewer"); $v->show_modal; ok($v->add_aspect("map_order"), "added a new aspect"); @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name chromosome map_order/], "returned aspects reflect the new addition"); $v->show_modal; $v->remove_aspect("chromosome"); @a = map { $_->aspect_name } sort { $a->position <=> $b->position } $v->get_aspects(); is_deeply(\@a, [qw/clone_name map_order/], "returned aspects reflect the removal"); $v->show_modal; 1; 63e_enumerate_available_views.t000444023532023421 157412121654174 21054 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; BEGIN { eval "use XML::LibXSLT"; if ($@) { plan skip_all => "Cannot load XML::LibXSLT: $@"; } else { plan tests => 5; use_ok('UR::Object::View::Default::Xsl', qw/url_to_type type_to_url/); } } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $type = URT::Thingy->__meta__; ok($type, 'got meta-object for URT::Thingy class'); my $view = $type->create_view(perspective => 'available-views', toolkit => 'xml'); isa_ok($view, 'UR::Object::View', 'created view for available views'); my $content = $view->content; ok($content, 'generated content'); my $err = $view->error_message; #errors if views do not have perspective and toolkit set appropriately ok(!$err, 'no errors in view creation'); 61_iterator.t000444023532023421 742212121654174 15332 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 20; my $dbh = &setup_classes_and_db(); # This tests creating an iterator and doing a regular get() # for the same stuff, and make sure they return the same things # create the iterator but don't read anything from it yet my $iter = URT::Thing->create_iterator(name => 'Bob'); ok($iter, 'Created iterator for Things named Bob'); my @objs; while (my $o = $iter->next()) { is($o->name, 'Bob', 'Got an object with name Bob'); push @objs, $o; } is(scalar(@objs), 2, '2 Things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,4], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Bob'], [name => 'Joe']]); ok($iter, 'Created an iterator for things named Bob or Joe'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 5, '5 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,4,6,8,10], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Joe', 'id <' => 8], [name => 'Bob', 'id >' => 3]]); ok($iter, 'Created an iterator for a more complicated OR rule'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 2, '2 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [4,6], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[name => 'Joe', data => 'foo'],[name => 'Bob']], -order => ['-data']); ok($iter, 'Created an iterator for an OR rule with with descending order by'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 3, '3 things returned by the iterator'); is_deeply( [ map { $_->id } @objs], [2,6,4], 'Got the right object IDs from the iterator'); @objs = (); $iter = URT::Thing->create_iterator(-or => [[ id => 2 ], [name => 'Bob', data => 'foo']]); ok($iter, 'Created an iterator for an OR rule with two ways to match the same single object'); while(my $o = $iter->next()) { push @objs, $o; } is(scalar(@objs), 1, 'Got one object back from the iterstor'); is_deeply( [ map { $_->id } @objs], [2], 'Gor the right object ID from the iterator'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table things (thing_id integer, name varchar, data varchar)'), 'Created things table'); my $insert = $dbh->prepare('insert into things (thing_id, name, data) values (?,?,?)'); foreach my $row ( ( [2, 'Bob', 'foo'], [4, 'Bob', 'baz'], [6, 'Joe', 'foo'], [8, 'Joe', 'bar'], [10, 'Joe','baz'], )) { unless ($insert->execute(@$row)) { die "Couldn't insert a row into 'things': $DBI::errstr"; } } $dbh->commit(); # Now we need to fast-forward the sequence past 4, since that's the highest ID we inserted manually my $sequence = URT::DataSource::SomeSQLite->_get_sequence_name_for_table_and_column('things', 'thing_id'); die "Couldn't determine sequence for table 'things' column 'thing_id'" unless ($sequence); my $id = -1; while($id <= 4) { $id = URT::DataSource::SomeSQLite->_get_next_value_from_sequence($sequence); } ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'data'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things'), 'Created class URT::Thing'); return $dbh; } 63b_view_with_subviews.t000444023532023421 477412121654174 17610 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; if ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'}) { plan tests => 11; } else { plan skip_all => 'works only with systems which have XML::LibXML and XML::LibXSLT.pm'; } #use File::Basename; #use lib File::Basename::dirname(__FILE__)."/../.."; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, ] }; my $p = Person->create(name => 'Fester', age => 99); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); my $pv = $p->create_view( toolkit => 'xml', aspects => [ 'name', 'age', { name => 'cats', perspective => 'default', toolkit => 'xml', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ] ); ok($pv, "got an XML view for the person"); my $pv_got_content = $pv->content; ok($pv_got_content, 'Person XML view generated some content'); SKIP: { skip "Need a better way to validate XML output",1; my $pv_expected_xml = ''; is($pv_got_content,$pv_expected_xml,"XML is as expected for the person view"); } my $c1v = $c1->create_view(toolkit => 'text'); ok($c1v, 'Created text view for a cat'); ok($c1v, "got a text view for one of the cats"); my $c1v_expected_text = "Cat '" . $c1->id . "' age: 2 fluf: 11 name: fluffy owner: Person '" . $p->id . "' age: 99 cats: Cat '" . $c1->id . "' (REUSED ADDR) Cat '".$c2->id."' age: 8 fluf: 22 name: nestor owner: Person '".$p->id."' (REUSED ADDR) name: Fester"; my $c1v_got_content = $c1v->content; ok($c1v_got_content, 'Cat text view generated some content'); chomp $c1v_got_content; # Convert all whitespace to a single space $c1v_got_content =~ s/\n/ /mg; $c1v_got_content =~ s/\s+/ /mg; is($c1v_got_content,$c1v_expected_text,"text is as expected for the cat view"); 50_unload_and_reload.t000444023532023421 2015512121654174 17147 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 86; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table thing (thing_id integer PRIMARY KEY, color varchar)'); $dbh->do("insert into thing values (1,'blue')"); $dbh->do("insert into thing values (2,'red')"); $dbh->do("insert into thing values (3,'green')"); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ color => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); my $query_count; URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }); # First, try with a single object $query_count = 0; my $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; my $cx = UR::Context->current; $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 with reload'); is($query_count, 1, 'make 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing->unload(); $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 after single-object unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $thing->unload(); $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 after single-object unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 after class unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); $thing = $cx->reload('URT::Thing', 1); ok($thing, 'Got thing with ID 1 after class unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; $thing = URT::Thing->get(1); ok($thing, 'Got thing with ID 1 again'); is($query_count, 0, 'Made no queries'); # Now try with all the objects of a class $query_count = 0; my @things = URT::Thing->get(); is(scalar(@things), 3, 'get() got 3 things'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'get() got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'got 3 things with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $_->unload() foreach @things; @things = URT::Thing->get(); ok(scalar(@things), 'Got thing with ID 1 after single-object unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; $_->unload() foreach @things; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'Got 3 things after single-object unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 3, 'Got 3 things after class unload with get()'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); $query_count = 0; URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'Got 3 things after class unload with reload'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things again'); is($query_count, 0, 'Made no queries'); # Try removing rows from the DB ok($dbh->do('delete from thing where thing_id = 1'), 'delete thing ID 1 from the database directly'); $query_count = 0; @things = URT::Thing->get(); is(scalar(@things), 3, 'got 3 things after delete with get'); is_deeply([sort map { $_->id } @things], [1,2,3], 'Object IDs were correct'); is($query_count, 0, 'Made no queries'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 2, 'reload still returns 3 things'); # ID 1 is gone is_deeply([sort map { $_->id } @things], [2,3], 'Object IDs were correct'); is($query_count, 2, 'Made 2 queries'); # 1 to get all the objects, another to verify ID 1 was gone $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 2, 'After class unload, get() returns 2 things'); is_deeply([sort map { $_->id } @things], [2,3], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing where thing_id = 2'), 'delete thing ID 2 from the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'After delete, reload returns 1 thing'); # ID 1 a and 2 are deleted is_deeply([sort map { $_->id } @things], [3], 'Object IDs were correct'); is($query_count, 2, 'Made 2 queries'); # 1 to get all the objects, another to verify ID 1 was gone $query_count = 0; URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'After delete, reload returns 1 thing'); is_deeply([sort map { $_->id } @things], [3], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do("insert into thing values (4,'orange')"), 'Insert a new row into the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 2, 'After DB insert and class unload, get() returns 2 things'); is_deeply([sort map { $_->id } @things], [3,4], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do("insert into thing values (5,'purple')"), 'Insert a new row into the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 3, 'After DB insert, reload returns 3 things'); is_deeply([sort map { $_->id } @things], [3,4,5], 'Object IDs were correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing'), 'delete all rows from the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 0, 'After DB delete and class unload, get() returns 0 things'); is($query_count, 1, 'Made 1 query'); #$DB::single=1; ok($dbh->do("insert into thing values (6,'black')"), 'Insert a new row into the database directly'); $query_count = 0; URT::Thing->unload(); @things = URT::Thing->get(); is(scalar(@things), 1, 'After DB delete and class unload, get() returns 1 thing'); is($things[0]->id, 6, 'Object ID was correct'); is($query_count, 1, 'Made 1 query'); ok($dbh->do('delete from thing'), 'again, delete all rows from the database directly'); @things = $cx->reload('URT::Thing'); is(scalar(@things), 0, 'reload returns no things'); URT::Thing->unload(); @things = $cx->reload('URT::Thing'); is(scalar(@things), 0, 'reload returns 0 things after unload'); ok($dbh->do("insert into thing values (7,'brown')"), 'Insert a new row into the database directly'); $query_count = 0; @things = $cx->reload('URT::Thing'); is(scalar(@things), 1, 'reload returns 1 thing'); is($query_count, 1, 'Made 1 query'); 50_get_and_reload.t000444023532023421 1340612121654174 16445 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 64; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; &setup_classes_and_db($dbh); foreach my $class ( 'URT::Thing', 'URT::SubclassedThing' ) { # try load() as an object method my $thing = $class->get(1); ok($thing, 'get() returned an object'); isa_ok($thing, $class); is($thing->name, 'Bob', 'name is correct'); is($thing->color, 'green', 'color is correct'); my $table_name = $class->__meta__->table_name; my $sth = $dbh->prepare("update $table_name set color = 'purple' where thing_id = 1"); ok($sth->execute(), 'updated the color'); $sth->finish; $dbh->commit; is($thing->color, 'green', 'Before load() it still has the old color'); my $cx = UR::Context->current; ok($cx->reload($thing), 'Called load()'); is($thing->color, 'purple', 'After load() it has the new color'); # try load() as a class method() my @things = $class->get(name => 'Fred'); is(scalar(@things),1, 'Got one thing named Fred'); is($things[0]->color, 'black', 'color is correct'); $sth = $dbh->prepare("update $table_name set color = 'yellow' where name = 'Fred'"); ok($sth->execute(), 'updated the color'); $sth->finish; $dbh->commit; @things = $cx->reload($class, name => 'Fred'); is(scalar(@things),1, 'Again, got one thing named Fred'); is($things[0]->color, 'yellow', 'new color is correct'); # try updating both the object and DB, and see if it'll reload @things = $class->get(3); is(scalar(@things),1, 'Got one thing with id 3'); is($things[0]->color, 'red', 'its color is red'); $sth = $dbh->prepare("update $table_name set color = 'orange' where thing_id = 3"); ok($sth->execute(), 'updated the color in the DB'); $sth->finish; $dbh->commit; ok($things[0]->color('blue'), 'updated the color on the object'); my $worked = eval { $cx->reload($things[0]) }; ok(! $worked, 'calling load() on the changed object correctly fails'); my $message = $@; $message =~ s/\s+/ /gm; like($message, qr/A change has occurred in the database for $class property 'color' on object ID 3 from 'red' to 'orange'. At the same time, this application has made a change to that value to 'blue'./, 'Error message looks correct'); is($things[0]->color, 'blue', 'color remains what we set it to'); #is($things[0]->{'db_committed'}->{'color'}, 'orange', 'db_committed for the color was updated to what we set the database to'); is(UR::Context->_get_committed_property_value($things[0],'color'), 'orange', 'db_committed for the color was updated to what we set the database to'); # We now have to make that last object look like it's unchanged or the next get() will # also throw an exception $things[0]->color($things[0]->{'db_committed'}->{'color'}); @things = $class->get(); is(scalar(@things), 3, 'get() with no filters returns all the things'); $sth = $dbh->prepare("update $table_name set color = 'white'"); ok($sth->execute(), 'updated the color for all things'); $sth->finish; $dbh->commit; $thing = $cx->reload($class, 1); is($thing->color, 'white', 'load() for thing_id 1 has the changed color'); @things = $cx->reload($class); foreach my $thing ( @things ) { is($thing->color, 'white', 'load() for all things has the changed color for this object'); } } sub setup_classes_and_db { my $dbh = shift; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created thing table'); my $ins_things = $dbh->prepare("insert into thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a thing'); } ok( $dbh->do("create table subclassed_thing (thing_id integer, name varchar, color varchar, type varchar)"), 'Created subclassed_thing table'); $ins_things = $dbh->prepare("insert into subclassed_thing (thing_id, name, type, color) values (?,?,?,?)"); foreach my $row ( ( [1, 'Bob', ,'Person', 'green' ], [2, 'Fred', 'Person', 'black' ], [3, 'Christine', 'Car', 'red' ] )) { ok($ins_things->execute(@$row), 'Inserted a subclassed_thing'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => ['name', 'color', 'type' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing', id_by => 'thing_id', has => ['name', 'color', 'type' ], is_abstract => 1, sub_classification_method_name => '_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'subclassed_thing', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing::Person', is => 'URT::SubclassedThing', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::SubclassedThing::Car', is => 'URT::SubclassedThing', data_source => 'URT::DataSource::SomeSQLite', ); } sub URT::SubclassedThing::_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type); } 80d_command_list.t000444023532023421 1343312121654174 16336 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 19; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Create database'); $dbh->do('create table workplace (workplace_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL)'); $dbh->do("insert into workplace values (1, 'Acme')"); $dbh->do("insert into workplace values (2, 'CoolCo')"); $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, workplace_id integer REFERENCES workplace(workplace_id))'); $dbh->do("insert into person values (1, 'Bob',1)"); $dbh->do("insert into person values (2, 'Fred',2)"); $dbh->do("insert into person values (3, 'Mike',1)"); $dbh->do("insert into person values (4, 'Joe',2)"); UR::Object::Type->define( class_name => 'URT::Workplace', id_by => 'workplace_id', has => [ name => { is => 'String' }, uc_name => { calculate_from => ['name'], calculate => q( return uc $name ) }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'workplace', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, uc_name => { calculate_from => ['name'], calculate => q( return uc $name ) }, workplace => { is => 'URT::Workplace', id_by => 'workplace_id' }, workplace_name => { via => 'workplace', to => 'name' }, workplace_uc_name => { via => 'workplace', to => 'uc_name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); my $counter = 0; sub URT::Person::a_sub { return $counter++ } my @p = URT::Person->__meta__->properties; my($fh,$output); $output = ''; open($fh, '>', \$output); # Query involving only one class, filter is a direct property, show has a calculated property my $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Workplace', filter => 'name=CoolCo', show => 'id,uc_name', output => $fh); ok($cmd, 'Create a lister command for Workplace. filter has direct, show has calculated'); ok($cmd->execute(), 'execute'); my $expected_output = <', \$output); # filter is a calculated property, show has both calculated and direct properties $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Workplace', filter => 'uc_name=COOLCO', show => 'id,uc_name,name', output => $fh); ok($cmd, 'Create a lister command for Workplace. filter has calculated, show has direct and calculated'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving two joined tables, filter is a via/to property, show has calculated and via/to properties $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'workplace_name=Acme', show => 'uc_name,workplace_uc_name', output => $fh); ok($cmd, 'Create a lister command for Person. filter has via/to, show has calculated and via/to'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving two joined tables, filter is a direct property, show has direct and via/to property $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'name~%o%', show => 'name,workplace_name', output => $fh); ok($cmd, 'Create a lister command for Person. filter has direct prop, show has direct and via/to'); ok($cmd->execute(), 'execute'); $expected_output = <', \$output); # Query involving one table and calling a subroutine directly $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', filter => 'name~%o%', show => 'name,$o->a_sub', output => $fh); ok($cmd, 'Create a lister command for Person with a subroutine in the show list'); ok($cmd->execute(), 'execute'); $expected_output = <<'EOS'; NAME ($O->A_SUB) ---- ----------- Bob 0 Joe 1 EOS is($output, $expected_output, 'Output is as expected'); $output = ''; open($fh, '>', \$output); $cmd = UR::Object::Command::List->create(subject_class_name => 'URT::Person', show => 'id,name,workplace_name', order_by => 'workplace_name', output => $fh); ok($cmd, 'Create a lister command for Person with a custom order-by'); ok($cmd->execute(), 'execute'); $expected_output = <<'EOS'; ID NAME WORKPLACE_NAME -- ---- -------------- 1 Bob Acme 3 Mike Acme 2 Fred CoolCo 4 Joe CoolCo EOS is($output, $expected_output, 'Output is as expected'); 49l_complicated_get_id_by_attribute.t000444023532023421 753012121654174 22237 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 7; # This test does a query involving an object accessor, where its id_by is indirect # through a filtered attribute table. We're making sure the join from person to # car_attribute includes conditions on both person.person_id = attr.value _and_ # attr.key = 'owner_id'. # # Without that second condition, the query for green cars joins to both people # because the green car has owner_id 1 and driver_id 2 my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table car (car_id integer not null primary key, color varchar not null)'); $dbh->do('create table car_attribute (attr_id integer not null primary key, car_id integer not null, key varchar, value varchar)'); # Bob and Fred are people $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into person values (2,'Fred')"); # Bob has a green and yellow car, Fred has a black car $dbh->do("insert into car values (1, 'green')"); $dbh->do("insert into car values (2, 'yellow')"); $dbh->do("insert into car values (3, 'black')"); $dbh->do("insert into car_attribute values (1, 1, 'owner_id', 1)"); $dbh->do("insert into car_attribute values (2, 1, 'driver_id', 2)"); # Also, Fred drives Bob's green car $dbh->do("insert into car_attribute values (4, 1, 'owner_id', 1)"); $dbh->do("insert into car_attribute values (7, 3, 'owner_id', 2)"); UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Text' }, ], has => [ name => { is => 'String' }, ], has_many => [ cars => { is => 'URT::Car', reverse_as => 'owner' }, car_colors => { via => 'cars', to => 'color' }, ] ); UR::Object::Type->define( class_name => 'URT::Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ color => { is => 'Text' }, owner_id => { is => 'Text', via => 'attributes', to => 'value', where => ['key' => 'owner_id']}, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], has_many => [ attributes => { is => 'URT::CarAttribute', reverse_as => 'car' }, ], ); UR::Object::Type->define( class_name => 'URT::CarAttribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car_attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ car => { is => 'URT::Car', id_by => 'car_id' }, key => { is => 'Text', }, value => { is => 'Text', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = URT::Person->get(car_colors => 'green'); is(scalar(@p), 1, 'Got one person with a green car'); is($query_count, 1, 'Made 1 query'); $query_count = 0; is($p[0]->name, 'Bob', 'It is the right person'); is($query_count, 0, 'Made 0 queries'); # If the query by car_colors worked properly, then this get() should not hit the DB # because it was loaded as part of the join connecting the car with its owner $query_count = 0; my $a = URT::CarAttribute->get(1); is($query_count, 0, 'Getting car attribute ID 1 took no DB queries'); # But this should hit the DB, because it was for the 'driver_id' attribute, not owner_id $query_count = 0; $a = URT::CarAttribute->get(2); is($query_count, 1, 'Getting car attribute ID 2 (driver_id) took 1 DB query'); 49j_complicated_get_join_ends_at_value_class.t000444023532023421 611112121654174 24073 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 9; # tests a get() where the a UR::Value-related property is in the hints list, but # in order to satisfy that hint, it needs to join to the attribute table to retrieve # the Value's ID # # Before the fix, the QueryPlan had a couple of issues # 1) The delegated properties loop would remove all joins through UR::Value classes, # leaving the @join list empty. At the snd of the delegation loop, # $last_class_object_excluding_inherited_joins had not been set, and so it dies # 2) UR::Object::Join::resolve_forward() would not recursivly find and joins required # to fulfill the id_by property. my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table disk (disk_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, disk_id integer references disk(disk_id), key varchar, value varchar)'); $dbh->do("insert into disk values (1,'boot')"); $dbh->do("insert into attribute values (3,1,'size_bytes', 2097152)"); # 2048K UR::Object::Type->define( class_name => 'Disk::Value::KBytes', is => 'UR::Value', ); sub Disk::Value::KBytes::__display_name__ { my $size = shift->id; my $kbytes = $size / 1024; return $kbytes."K"; } UR::Object::Type->define( class_name => 'Disk', data_source => 'URT::DataSource::SomeSQLite', table_name => 'disk', id_by => [ disk_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'disk', is_many => 1 }, size => { via => 'attributes', to => 'value', where => [key => 'size_bytes'] }, pretty_size_kbytes => { is => 'Disk::Value::KBytes', id_by => 'size' }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ disk => { is => 'Disk', id_by => 'disk_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @d = Disk->get(-hints => ['pretty_size_kbytes']); is(scalar(@d), 1, 'Got the object'); is($query_count, 1, 'Made one query'); $query_count = 0; my $value_obj = $d[0]->pretty_size_kbytes; ok($value_obj, 'Got the value object for size'); is($query_count, 0, 'Made no queries'); $query_count = 0; is($value_obj->id, $d[0]->size, 'The ID of the value object matches the original object size'); is($query_count, 0, 'Made no queries'); $query_count = 0; is($value_obj->__display_name__, "2048K", '__display_name__ for Value object is correct'); is($query_count, 0, 'Made no queries'); 65_reload_with_changing_db_data.t000444023532023421 2255712121654174 21330 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 270; use URT::DataSource::SomeSQLite; # This test uses 3 independent groups of classes/tables: # 1) One class where no subclassing is involved (URT::Thing uses table thing) # 2) A pair of classes where we need to do a join to get the subclassed data # URT::Fruit uses the fruit table. URT::Apple is its subclass and uses table # apple # 3) A pair of classes where the child class has no table of its own. # URT::Vehicle uses table vehicle. URT::Car is its subclass and has no table my $dbh = &setup_classes_and_db(); # The test messes with the 'value' property/column. This hash maps the class name # with which table contains the 'value' column my %table_for_class = ('URT::Thing' => 'thing', 'URT::Fruit' => 'apple', 'URT::Apple' => 'apple', 'URT::Vehicle' => 'vehicle', 'URT::Car' => 'vehicle', ); # Context exception messages complain about the class the data originally comes from my %complaint_class = ('URT::Thing' => 'URT::Thing', 'URT::Fruit' => 'URT::Apple', 'URT::Apple' => 'URT::Apple', 'URT::Vehicle' => 'URT::Vehicle', 'URT::Car' => 'URT::Vehicle', ); my $obj_id = 1; foreach my $test_class ( 'URT::Thing', 'URT::Fruit', 'URT::Apple', 'URT::Vehicle', 'URT::Car') { #diag("Working on class $test_class"); UR::DBI->no_commit(0); my $test_table = $table_for_class{$test_class}; my $this_pass_obj_id = $obj_id++; my $thing = $test_class->get($this_pass_obj_id); ok($thing, "Got a $test_class object"); is($thing->value, 1, 'its value is 1'); my $cx = UR::Context->current(); ok($cx, 'Got the current context'); # First test. Make no changes and reload the object ok(eval { $cx->reload($thing) }, 'Reloaded object after no changes'); is($@, '', 'No exceptions during reload'); ok(!scalar($thing->__changes__), 'No changes, as expected'); # Next test, Make a change to the database, no change to the object and reload # It should update the object's value to match the newly reloaded DB data ok($dbh->do("update $test_table set value = 2 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 2'); ok(eval { $cx->reload($thing) }, 'Reloaded object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 2, 'its value is now 2'); ok(!scalar($thing->__changes__), 'No changes. as expected'); # make a change to the object, no change to the DB ok($thing->value(3), 'Changed the object value to 3'); is(scalar($thing->__changes__), 1, 'One change, as expected'); ok(eval { $cx->reload($thing) },' Reload object'); is($@, '', 'No exceptions during reload'); is($thing->value, 3, 'Value is still 3'); is(scalar($thing->__changes__), 1, 'Still one change, as expected'); # Make a change to the DB, and the exact same change to the object ok($dbh->do("update $test_table set value = 3 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 3'); ok($thing->value(3), "Changed the object's value to 3"); ok($thing->__changes__, 'Before reloading, object says it has changes'); ok(eval { $cx->reload($thing) },'Reloaded object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 3, 'Value is 3'); ok(! scalar($thing->__changes__), 'After reloading, object says it has no changes'); # Make a change to the DB data, and a different cahange to the object. This should fail ok($dbh->do("update $test_table set value = 4 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 4'); ok($thing->value(5), "Changed the object's value to 5"); ok(! eval { $cx->reload($thing) },'Reloading fails, as expected'); my $message = $@; $message =~ s/\s+/ /gm; # collapse whitespace my $complaint_class = $complaint_class{$test_class}; like($message, qr/A change has occurred in the database for $complaint_class property 'value' on object ID $this_pass_obj_id from '3' to '4'. At the same time, this application has made a change to that value to '5'./, 'Exception message looks correct'); is($thing->value, 5, 'Value is 5'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(6), "Changed the object's value to 6"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 6 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 6'); ok(eval { $cx->reload($thing) },'Reloading object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 6, 'Value is 6'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(7), "Changed the object's value to 7"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 7 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 7'); ok($thing->value(8), 'Changed object value to 8'); ok(eval { $cx->reload($thing) },'Reloading object again'); is($@, '', 'No exceptions during reload'); is($thing->value, 8, 'Value is 8'); ok(UR::DBI->no_commit(1), 'Turned on no_commit'); ok($thing->value(9), "Changed the object's value to 9"); ok(UR::Context->commit(), 'calling commit()'); ok($dbh->do("update $test_table set value = 10 where thing_id = $this_pass_obj_id"), 'Updated value for thing in the DB to 10'); ok($thing->value(11), 'Changed object value to 11'); ok(! eval { $cx->reload($thing) },'Reloading fails, as expected'); $message = $@; $message =~ s/\s+/ /gm; # collapse whitespace like($message, qr/A change has occurred in the database for $complaint_class property 'value' on object ID $this_pass_obj_id from '9' to '10'. At the same time, this application has made a change to that value to '11'/, 'Exception message looks correct'); is($thing->value, 11, 'Value is 11'); } sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer PRIMARY KEY, value integer)"), 'created thing table'); ok($dbh->do("create table fruit (thing_id integer PRIMARY KEY, fruitvalue integer) "), 'created fruit table'); ok($dbh->do("create table apple(thing_id integer PRIMARY KEY references fruit(thing_id), value integer)"), 'created apple table'); ok($dbh->do("create table vehicle (thing_id integer PRIMARY KEY, value integer) "), 'created vehicle table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5 ) { # We need one item for each class under test at the top $sth->execute($val,1); } $sth->finish; my $fruitsth = $dbh->prepare('insert into fruit values (?,?)'); ok($fruitsth, 'Prepared fruit insert statement'); my $applesth = $dbh->prepare('insert into apple values (?,?)'); ok($applesth, 'Prepared apple insert statement'); my $vehiclesth = $dbh->prepare('insert into vehicle values (?,?)'); ok($vehiclesth, 'Prepared vehicle insert statement'); foreach my $val ( 1,2,3,4,5 ) { # one item for each class here, too $fruitsth->execute($val,1); $applesth->execute($val,1); $vehiclesth->execute($val,1); } $fruitsth->finish; $applesth->finish; $vehiclesth->finish; ok($dbh->commit(), 'DB commit'); # A class we can load directly UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); # A pair of classes, one that inherits from another. The child class # has a table that gets joined sub URT::Fruit::resolve_subclass_name { return 'URT::Apple'; # All are Apples for this test } UR::Object::Type->define( class_name => 'URT::Fruit', sub_classification_method_name => 'resolve_subclass_name', id_by => 'thing_id', has => [ 'fruitvalue' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'fruit', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Apple', is => 'URT::Fruit', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'apple', ); # Another pair of classes. This time, the child class does not have its own table. sub URT::Vehicle::resolve_subclass_name { return 'URT::Car'; # All are Cars for this test } UR::Object::Type->define( class_name => 'URT::Vehicle', sub_classification_method_name => 'resolve_subclass_name', id_by => 'thing_id', has => [ 'value' ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'vehicle', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Car', is => 'URT::Vehicle', ); return $dbh; } 04f_filemux_sync_database.t000444023532023421 1102612121654174 20210 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 36; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeFileMux; my $ds = URT::DataSource::SomeFileMux->get(); ok($ds, 'got the datasource object'); &setup_files_and_classes($ds); my $thing1 = URT::Thing->get(thing_id => 1, thing_type => 'person'); ok($thing1, 'got an object'); ok($thing1->thing_color('changed'), 'Changed its color'); my $thing2 = URT::Thing->get(thing_id => 10, thing_type => 'robot'); ok($thing2, 'Got another object'); ok($thing2->thing_name('TomTom'), 'Changed its name'); my $thing3 = URT::Thing->get(thing_id => 2, thing_type => 'person'); ok($thing3, 'Got a third thing'); ok($thing3->delete, 'Deleted it'); my $new1 = URT::Thing->create(thing_id => 3, thing_name => 'Shaggy', thing_color => 'green', thing_type => 'person'); ok($new1, 'Created a new thing'); my $new2 = URT::Thing->create(thing_id => 9, thing_name => 'Fred', thing_color => 'white', thing_type => 'person'); ok($new2, 'Created a new thing 2'); my $new3 = URT::Thing->create(thing_id => 0, thing_name => 'Velma', thing_color => 'red', thing_type => 'person'); ok($new3, 'Created a new thing 3'); my $new4 = URT::Thing->create(thing_id => 11, thing_name => 'Robbie', thing_color => 'black', thing_type => 'robot'); ok($new4, 'Created a new thing 4'); my $new5 = URT::Thing->create(thing_id => 20, thing_name => 'Scooby', thing_color => 'brown', thing_type => 'animal'); ok($new5, 'Created a new thing 5'); ok(UR::Context->commit(), 'Commit'); &check_files($ds); foreach my $obj ( $new1, $new2, $new3, $new4, $new5 ) { ok(exists($obj->{'db_committed'}), "New object now has a 'db_committed' hash key") } sub check_files { my $ds = shift; my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $f = IO::File->new("$dir/person"); ok($f, 'Opened file for person data'); my $line = $f->getline(); is($line, qq(0\tVelma\tred\n), 'Line 0'); $line = $f->getline(); is($line, qq(1\tJoel\tchanged\n), 'Line 1'); $line = $f->getline(); is($line, qq(3\tShaggy\tgreen\n), 'Line 2'); $line = $f->getline(); is($line, qq(4\tFrank\tblack\n), 'Line 3'); $line = $f->getline(); is($line, qq(5\tClayton\tgreen\n), 'Line 4'); $line = $f->getline(); is($line, qq(9\tFred\twhite\n), 'Line 5'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); $f = IO::File->new("$dir/robot"); ok($f, 'Opened file for robot data'); $line = $f->getline(); is($line, qq(8\tCrow\tgold\n), 'Line 0'); $line = $f->getline(); is($line, qq(10\tTomTom\tred\n), 'Line 1'); $line = $f->getline(); is($line, qq(11\tRobbie\tblack\n), 'Line 3'); $line = $f->getline(); is($line, qq(12\tGypsy\tpurple\n), 'Line 2'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); $f = IO::File->new("$dir/animal"); ok($f, 'Opened file for animal data'); $line = $f->getline(); is($line, qq(20\tScooby\tbrown\n), 'Line 0'); $line = $f->getline(); is($line, undef, 'end of file'); $f->close(); unlink("$dir/person", "$dir/robot", "$dir/animal"); } sub setup_files_and_classes { my $ds = shift; my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $delimiter = $ds->delimiter; unlink("$dir/person", "$dir/robot", "$dir/animal"); my $file = "$dir/person"; my $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(1 Joel grey)),"\n"); $f->print(join($delimiter, qw(2 Mike blue)),"\n"); $f->print(join($delimiter, qw(4 Frank black)),"\n"); $f->print(join($delimiter, qw(5 Clayton green)),"\n"); $f->close(); $file = "$dir/robot"; $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(8 Crow gold)),"\n"); $f->print(join($delimiter, qw(10 Tom red)),"\n"); $f->print(join($delimiter, qw(12 Gypsy purple)),"\n"); $f->close(); my $c = UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, thing_type => { is => 'String' }, ], table_name => 'wefwef', data_source => 'URT::DataSource::SomeFileMux', ); ok($c, 'Created class'); } 00_load.t000444023532023421 56112121654174 14366 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; $ENV{'CALL_COUNT_OUTFILE'} = '/dev/null'; # so Devel::Callcount won't drop a file in the tree plan tests => 2; use_ok( 'UR' ); use_ok( 'UR::All' ); note( "Testing UR $UR::VERSION, Perl $], $^X" ); 84b_implied_properties.t000444023532023421 354112121654174 17545 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use above 'UR'; use Test::More tests => 9; UR::Object::Type->define(class_name => 'Sandwich'); UR::Object::Type->define(class_name => 'Drink'); UR::Object::Type->define( class_name => 'Combo', id_by => [ sandwich => { is => 'Sandwich' }, drink => { is => 'Drink' }, ], ); UR::Object::Type->define( class_name => 'Order', has => [ sandwich => { is => 'Sandwich', id_by => 'sandwich_id' }, drink => { is => 'Drink' }, ], ); UR::Object::Type->define( class_name => 'BuggedOrder', has => [ # sandwich has to have the id_by here in order to trigger the bug sandwich => { is => 'Sandwich', id_by => 'sandwich_id' }, drink => { is => 'Drink' }, # yes, drink_id is ommitted here ], has_optional => [ combo => { is => 'Combo', id_by => ['sandwich_id', 'drink_id'], # This drink_id is not related to 'drink' above }, ], ); my $sandwich = Sandwich->create; isa_ok($sandwich, 'Sandwich', 'sandwich'); my $drink = Drink->create; isa_ok($drink, 'Drink', 'drink'); my $ok_order = Order->create(sandwich => $sandwich, drink => $drink); isa_ok($ok_order, 'Order', 'ok_order'); is($ok_order->__meta__->property('sandwich')->is_optional, 0, 'sandwich is not optional'); my $order = BuggedOrder->create(sandwich => $sandwich, drink => $drink); isa_ok($order, 'BuggedOrder', 'order'); my $order_meta = $order->__meta__; is($order_meta->property('sandwich_id')->is_optional, 0, 'sandwich_id is not optional'); is($order_meta->property('sandwich')->is_optional, 0, 'sandwich is not optional'); is($order_meta->property('drink')->is_optional, 0, 'drink is not optional'); # because drink_id isn't mentioned in the definition of drink, but is for combo is($order_meta->property('drink_id')->is_optional, 1, 'drink_id is optional'); 91d_basic_set.t000444023532023421 431012121654174 15575 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 28; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; ok(UR::Object::Type->define( class_name => 'URT::ThingNoDataSource', id_by => [ name => { is => 'String' }, ], has => [ group_name => { is => 'String' }, total_size => { is => 'Integer' }, ], ), 'Define class without a data source'); ok(UR::Object::Type->define( class_name => 'URT::ThingWithDataSource', id_by => [ name => { is => 'String' }, ], has => [ group_name => { is => 'String' }, total_size => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'things', ), 'Define class with a data source'); my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); $dbh->do('create table things (name varchar not null primary key, group_name varchar, total_size integer)'); foreach my $test_class ( 'URT::ThingNoDataSource', 'URT::ThingWithDataSource' ) { ok($test_class->create(name => 'a', group_name => '1', total_size => 10), "create $test_class a"); ok($test_class->create(name => 'b', group_name => '1', total_size => 20), "create $test_class b"); ok($test_class->create(name => 'c', group_name => '2', total_size => 30), "create $test_class c"); ok($test_class->create(name => 'd', group_name => '2', total_size => 40), "create $test_class d"); #my @sets = $test_class->get(-group_by => ['group_name'], -order_by => ['group_name'] ); my @sets = $test_class->define_set()->group_by('group_name'); is(scalar(@sets), 2, 'Got two sets back grouped by group_name'); is($sets[0]->group_name, '1', 'Group name 1 is first'); is($sets[0]->min('total_size'), 10, '10 is min total_size'); is($sets[0]->max('total_size'), 20, '20 is max total_size'); is($sets[0]->sum('total_size'), 30, '30 is sum total_size'); is($sets[1]->group_name, '2', 'Disk group 2 is second'); is($sets[1]->min('total_size'), 30, '30 is min total_size'); is($sets[1]->max('total_size'), 40, '40 is max total_size'); is($sets[1]->sum('total_size'), 70, '70 is sum total_size'); } 46_meta_property_relationships.t000444023532023421 2160512121654174 21361 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse Test::More; use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; plan tests => 56; ok( UR::Object::Type->define( class_name => 'URT::Related', id_by => ['rel_id_a', 'rel_id_b'], # purposefully make the complete definitions for the ID properties # in a different order. The real order should be whatever was in id_by has => [ rel_id_b => { is => 'Integer' }, related_value => { is => 'String' }, rel_id_a => { is => 'Integer' }, ], ), 'Define related class'); ok( UR::Object::Type->define( class_name => 'URT::Parent', id_by => [ parent_id => { is => 'Integer' } ], has => [ parent_value => { is => 'String' }, related_object => { is => 'URT::Related', id_by => ['rel_id_a', 'rel_id_b']}, related_value => { via => 'related_object', to => 'related_value' }, ] ), 'Define parent class'); ok( UR::Object::Type->define( class_name => 'URT::Child', is => 'URT::Parent', id_by => [ child_id => { is => 'Integer' } ], has => [ child_value => { is => 'String' }, ], ), 'Define child class'); my $parent_meta = URT::Parent->__meta__; ok($parent_meta, 'Parent class metadata'); my @props = $parent_meta->direct_id_property_metas(); is(scalar(@props), 1, 'Parent class has 1 ID property'); my @names = map { $_->property_name } @props; my @expected = qw(parent_id); is_deeply(\@names, \@expected, 'Property names match'); my $related_meta = URT::Related->__meta__; ok($related_meta, 'Related class metadata'); @props = $related_meta->direct_id_property_metas(); is(scalar(@props), 2, 'Related class has 2 ID properties'); @names = map { $_->property_name } @props; @expected = qw(rel_id_a rel_id_b); is_deeply(\@names, \@expected, 'Property names match'); my $prop = $related_meta->property_meta_for_name('rel_id_a'); # is_id actually returns "0 but true" for the first one is($prop->is_id + 0, 0, 'id position for Related property rel_id_a is 0'); $prop = $related_meta->property_meta_for_name('rel_id_b'); is($prop->is_id, 1, 'id position for Related property rel_id_b is 1'); $prop = $related_meta->property_meta_for_name('related_value'); is($prop->is_id, undef, 'id position for Related property rel_id_b is undef'); @props = $parent_meta->direct_property_metas(); is(scalar(@props), 6, 'Parent class has 6 direct properties with direct_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(parent_id parent_value rel_id_a rel_id_b related_object related_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $parent_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); $prop = $parent_meta->direct_property_meta(property_name => 'related_value'); ok($prop, 'singular property accessor works'); my $child_meta = URT::Child->__meta__; ok($child_meta, 'Child class metadata'); @props = $child_meta->direct_property_metas(); is(scalar(@props), 2, 'Child class has 2 direct properties'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $child_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); @props = $child_meta->all_property_metas(); is(scalar(@props), 9, 'Child class has 9 properties through all_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names,\@expected, 'Property names check out'); # properties() only returns properties with storage, not object accessors or the property named 'id' @props = $child_meta->properties(); is(scalar(@props), 9, 'Child class has 9 properties through properties()') or diag join(", ",$child_meta->property_names); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names,\@expected, 'Property names check out') or diag "@names\n@expected\n"; $prop = $child_meta->direct_property_meta(property_name => 'related_value'); ok(! $prop, "getting a property defined on parent class through child's direct_property_meta finds nothing"); $prop = $child_meta->property_meta_for_name('related_value'); ok($prop, "getting a property defined on parent class through child's property_meta_for_name works"); ok(UR::Object::Property->create( class_name => 'URT::Child', property_name => 'extra_property', data_type => 'String'), 'Created an extra property on Child class'); @props = $child_meta->properties(); is(scalar(@props), 10, 'Child class now has 10 properties()'); @names = map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out') or diag ("@names\n@expected\n"); @props = $child_meta->direct_property_metas(); is(scalar(@props), 3, 'Child class now has 3 direct_property_metas()'); @props = $child_meta->all_property_metas(); is(scalar(@props), 10, 'Child class now has 10 properties through all_property_names()'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out'); ok(UR::Object::Property->create( class_name => 'URT::Parent', property_name => 'parent_extra', data_type => 'String'), 'Created extra property on parent class'); @props = $parent_meta->direct_property_metas(); is(scalar(@props), 7, 'Parent class now has 7 direct properties with direct_property_metas'); @names = sort map { $_->property_name } @props; @expected = qw(parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value); is_deeply(\@names, \@expected, 'Property names check out'); @names = sort $parent_meta->direct_property_names; is_deeply(\@names, \@expected, 'Property names from direct_property_names are correct'); @props = $child_meta->properties(); is(scalar(@props), 11, 'Child class now has 11 properties()'); @names = map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out') or diag "@names\n@expected\n"; @props = $child_meta->all_property_metas(); is(scalar(@props), 11, 'Child class now has 11 properties through all_property_names()'); @names = sort map { $_->property_name } @props; @expected = qw(child_id child_value extra_property id parent_extra parent_id parent_value rel_id_a rel_id_b related_object related_value), is_deeply(\@names, \@expected, 'Property names check out'); @props = $parent_meta->property_meta_for_name('related_object'); is(scalar(@props), 1, 'Parent class has a property called related_object'); is($props[0]->property_name, 'related_object', 'Got the right property'); @props = $child_meta->property_meta_for_name('related_object'); is(scalar(@props), 1, 'Child class also has a property called related_object'); is($props[0]->property_name, 'related_object', 'Got the right property'); @props = $child_meta->property_meta_for_name('related_object.related_value'); is(scalar(@props), 2, 'Got 2 properties involved for related_object.related_value on the child class'); is($props[0]->class_name, 'URT::Parent', 'First property meta\'s class_name is correct'); is($props[0]->property_name, 'related_object', 'First property meta\'s property_name is correct'); is($props[1]->class_name, 'URT::Related', 'second class_name for that property is correct'); is($props[1]->property_name, 'related_value', 'second property_name is correct'); @props = $child_meta->property_meta_for_name('non_existent'); is(scalar(@props), 0, 'No property found for name \'non_existent\''); @props = $child_meta->property_meta_for_name('non_existent.also_non_existent'); is(scalar(@props), 0, 'No property found for name \'non_existent.also_non_existent\''); @props = $child_meta->property_meta_for_name('related_object.also_non_existent'); is(scalar(@props), 0, 'No property found for name \'related_object.also_non_existent\''); my @classes = $child_meta->parent_class_metas(); is(scalar(@classes), 1, 'Child class has 1 parent class'); @names = map { $_->class_name } @classes; @expected = qw( URT::Parent ); is_deeply(\@names, \@expected, 'parent class names check out'); @names = sort $child_meta->ancestry_class_names; is(scalar(@names), 2, 'Child class has 2 ancestry classes'); @expected = qw( UR::Object URT::Parent ); is_deeply(\@names, \@expected, 'Class names check out'); 03b_rule_subsets.t000444023532023421 1010312121654174 16364 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 24; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, ], }; my($r1, $r2); $r1 = URT::FancyItem->define_boolexpr(); ok($r1->is_subset_of($r1), 'boolexpr with no filters is a subset of itself'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r1), 'boolexpr with one filter is a subset of itself'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'Two rules with the same filters are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); ok($r1->is_subset_of($r2), 'Two rules with the same filters are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(group => 'home', name => 'Bob'); ok($r1->is_subset_of($r2), 'Two rules with the same filters in a different order are a subset'); ok($r2->is_subset_of($r1), 'Two rules with the same filters in a different order are a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Fred'); ok(! $r1->is_subset_of($r2), 'Rule with different value for same filter name is not a subset'); ok(! $r2->is_subset_of($r1), 'Rule with different value for same filter name is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(group => 'Bob'); ok(! $r1->is_subset_of($r2), 'Rule with different param names and same value is not a subset'); ok(! $r2->is_subset_of($r1), 'Rule with different param names and same value is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(); ok($r1->is_subset_of($r2), 'one filter is a subset of no filters'); ok(! $r2->is_subset_of($r1), 'converse is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'Rule with two filters is subset of rule with one filter'); ok(! $r2->is_subset_of($r1),' Rule with one filter is not a subset of rule with two filters'); $r1 = URT::FancyItem->define_boolexpr(); $r2 = URT::Item->define_boolexpr(); ok($r1->is_subset_of($r2), 'subset by inheritance with no filters'); ok(! $r2->is_subset_of($r1), 'ancestry is not a subset'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob'); $r2 = URT::Item->define_boolexpr(name => 'Bob'); ok($r1->is_subset_of($r2), 'inheritance and one filter is subset'); ok(! $r2->is_subset_of($r1), 'ancestry and one filter is not a subset'); $r1 = URT::FancyItem->define_boolexpr(name => 'Bob', group => 'home'); $r2 = URT::Item->define_boolexpr(group => 'home', name => 'Bob'); ok($r1->is_subset_of($r2), 'inheritance and two filters in different order is subset'); ok(! $r2->is_subset_of($r1), 'ancestry and two filters in different order is not a subset'); $r1 = URT::Item->define_boolexpr(name => 'Bob'); $r2 = URT::UnrelatedItem->define_boolexpr(name => 'Bob'); ok(! $r1->is_subset_of($r2), 'Rules on unrelated classes with same filters is not a subset'); ok(! $r2->is_subset_of($r1), 'Rules on unrelated classes with same filters is not a subset'); 47c_is_many_accessor_with_id_class_by.t000444023532023421 434712121654174 22562 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 31; class URT::Note { id_by => [ id => { is => 'Number', len => 10 }, ], has => [ subject_class_name => { is => 'Text', len => 255 }, subject_id => { is => 'Text', len => 255 }, subject => { is => 'UR::Object', id_class_by => 'subject_class_name', id_by => 'subject_id' }, editor_id => { is => 'Text', len => 200 }, entry_date => { is => 'Date' }, header_text => { is => 'Text', len => 200 }, ], has_optional => [ body_text => { is => 'Text', len => 1000 }, ], }; class URT::Notable { is_abstract => 1, has => [ notes => { is => 'URT::Note', is_many => 1, reverse_as => 'subject', }, ], }; class URT::Foo { is => 'URT::Notable', }; class URT::Bar { is => 'URT::Foo' }; class URT::Baz { is => 'URT::Foo' }; ok(URT::Foo->isa("URT::Notable")); my $o1 = URT::Bar->create(100); ok($o1, "created a test notable object"); my $o2 = URT::Baz->create(200); ok($o2, "created another test notable object"); my @n; my $n; @n = $o1->notes; is(scalar(@n),0,"no notes at start"); @n = $o2->notes; is(scalar(@n),0,"no notes at start"); for my $o ($o1,$o2) { $n = $o->add_note( header_text => "head1", body_text => "body1", ); ok($n, "added a note"); is($n->header_text, 'head1', 'header is okay'); is($n->body_text, 'body1', 'body is okay'); #print Data::Dumper::Dumper($n); $n = $o->add_note( header_text => "head2", body_text => "body2", ); ok($n, "added a note"); is($n->header_text, 'head2', 'header is okay'); is($n->body_text, 'body2', 'body is okay'); #print Data::Dumper::Dumper($n); }; for my $o ($o1,$o2) { my @n = $o->notes; is(scalar(@n),2,"got two notes for the object"); for my $n (@n) { is($n->subject_class_name,ref($o),"class is set"); is($n->subject_id,$o->id,"id is set"); is($n->subject,$o,"object access works"); } } 1; 70_command_arg_processing.t000444023532023421 1604012121654174 20220 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use UR; use Test::More tests => 78; # tests parsing of command-line options class Cmd::Module::V1 { is => 'Command::V1', has => [ a_string => { is => 'String' }, a_number => { is => 'Number' }, opt_string => { is => 'String', is_optional => 1 }, opt_number => { is => 'Number', is_optional => 1 }, optnumber => { is => 'Number', is_optional => 1 }, ], }; class Cmd::Module::V2 { is => 'Command::V2', has => [ a_string => { is => 'String' }, a_number => { is => 'Number' }, opt_string => { is => 'String', is_optional => 1 }, opt_number => { is => 'Number', is_optional => 1 }, optnumber => { is => 'Number', is_optional => 1 }, ], }; # Commands dump errors about missing required properties # we don't care about those problems Cmd::Module::V1->dump_error_messages(0); Cmd::Module::V2->dump_error_messages(0); foreach my $the_class ( qw( Cmd::Module::V1 Cmd::Module::V2 )) { my($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah --a-number 123)); is($class,$the_class, 'Parse args got correct class'); is_deeply($params, { a_string => 'blah', a_number => 123 }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string=blah --a-number=123)); is($class,$the_class, 'Parse args got correct class using = in cmdline'); is_deeply($params, { a_string => 'blah', a_number => 123 }, 'Params are correct'); my $errors; ($class,$params,$errors) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah)); is($class,$the_class, 'Parse args got correct class using = in cmdline'); is_deeply($params, { a_string => 'blah'}, 'Params are correct'); my $r = $class->execute(%$params); ok(!$r, "result works"); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class where value contains ='); is_deeply($params, { a_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string=something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with = where value contains ='); is_deeply($params, { a_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with optional param where value contains ='); is_deeply($params, { opt_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string=something=with=equals-signs)); is($class,$the_class, 'Parse args got correct class with optional param = where value contains ='); is_deeply($params, { opt_string => 'something=with=equals-signs'}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--a-string blah --opt-string foo)); is($class,$the_class, 'Parse args got correct class with is_optional item'); is_deeply($params, { a_string => 'blah', opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string foo --opt-number 4)); is($class,$the_class, 'Parse args got correct class with two is_optional items'); is_deeply($params, { opt_number => 4, opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string=foo --opt-number=4)); is($class,$the_class, 'Parse args got correct class with = and two is_optional items'); is_deeply($params, { opt_number => 4, opt_string => 'foo' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv('--opt-string', '', '--opt-number', ''); is($class,$the_class, 'Parse args got correct class with two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string='' --opt-number='')); is($class,$the_class, 'Parse args got correct class with = and two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string="" --opt-number="")); is($class,$the_class, 'Parse args got correct class with = and two optional items with no value'); is_deeply($params, { opt_number => '', opt_string => '' }, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number 4)); is($class,$the_class, 'Parse args got correct class with one optional number'); is_deeply($params, { opt_number => 4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number=4)); is($class,$the_class, 'Parse args got correct class with = and one optional number'); is_deeply($params, { opt_number => 4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number=-422)); is($class,$the_class, 'Parse args got correct class with = and one optional negative number'); is_deeply($params, { opt_number => -422}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-number -4)); is($class,$the_class, 'Parse args got correct class with and one optional negative number'); is_deeply($params, { opt_number => -4}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--optnumber -422)); is($class,$the_class, 'Parse args got correct class with and one optional negative number'); is_deeply($params, { optnumber => -422}, 'Params are correct'); ($class,$params) = $the_class->resolve_class_and_params_for_argv(qw(--opt-string -4)); is($class,$the_class, 'Parse args got correct class with and one optional string where value is a negative number'); is_deeply($params, { opt_string => -4}, 'Params are correct'); } 04a_sqlite_init_db_internal.t000555023532023421 604112121654174 20523 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Test the internal dumping code for systems that do not have sqlite3 in the PATH my @db_creation_text = ( q(BEGIN TRANSACTION;), q(CREATE TABLE bar (bar_id integer PRIMARY KEY, some_data varchar);), q(INSERT INTO "bar" VALUES(1,'Hi there');), q(INSERT INTO "bar" VALUES(2,'blahblah');), q(INSERT INTO "bar" VALUES(3,null);), q(CREATE TABLE foo (foo_id_1 integer, foo_id_2 integer, PRIMARY KEY (foo_id_1, foo_id_2));), q(INSERT INTO "foo" VALUES(1,2);), q(INSERT INTO "foo" VALUES(2,3);), q(INSERT INTO "foo" VALUES(4,5);), q(COMMIT;), ); if (defined URT::DataSource::SomeSQLite->_singleton_object->_get_foreign_key_setting) { # If DBD::SQLite supports foreign keys, then the dump file will have this line unshift @db_creation_text, q(PRAGMA foreign_keys = OFF;); plan tests => 21; } else { plan tests => 20; } my $dump_file = URT::DataSource::SomeSQLite->_data_dump_path(); my $fh = IO::File->new($dump_file, 'w'); ok($fh, "Opened dump file for writing"); unless ($fh) { diag "Can't open $dump_file for writing: $!"; } $fh->print(join("\n", @db_creation_text), "\n"); $fh->close(); { local $ENV{'PATH'} = '/nonexistent'; # These _should_ ensure that we'll re-initialize the DB from the dump my $db_file = URT::DataSource::SomeSQLite->server; unlink($db_file); URT::DataSource::SomeSQLite->disconnect; note("initializing DB"); URT::DataSource::SomeSQLite->_init_database(); note("db file is $db_file"); my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); # Try getting some data my @row = $dbh->selectrow_array('select * from foo where foo_id_1 = 1'); ok(($row[0] == 1 and $row[1] == 2), 'Got row from table foo'); @row = $dbh->selectrow_array('select * from foo where foo_id_1 = 2'); ok(($row[0] == 2 and $row[1] == 3), 'Got row from table foo'); @row = $dbh->selectrow_array('select * from bar where bar_id = 1'); ok(($row[0] == 1 and $row[1] eq 'Hi there'), 'Got row from table bar'); @row = $dbh->selectrow_array('select * from bar where bar_id = 3'); ok(($row[0] == 3 and !defined($row[1])) , 'Got row from table bar'); # truncate the dump file to 0 bytes { my $fh = IO::File->new($dump_file, '>'); $fh->close(); } ok(URT::DataSource::SomeSQLite->_singleton_object->_dump_db_to_file_internal(), 'Call force re-creation of the dump file'); ok((-r $dump_file and -s $dump_file), 'Re-created dump file'); $fh = IO::File->new($dump_file); ok($fh, "Opened dump file for reading"); for(my $i = 0; $i < @db_creation_text; $i++) { my $line = $fh->getline(); chomp $line; is($line, $db_creation_text[$i], 'DB dump test line ' . ($i+1) . ' is correct'); } } 30_default_values.t000444023532023421 2334512121654174 16522 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 84; UR::Object::Type->define( class_name => 'URT::Parent', has => [ name => { is => 'String', default_value => 'Anonymous' }, ], ); UR::Object::Type->define( class_name => 'URT::Child', is => 'URT::Parent', has => [ color => { is => 'String', default_value => 'clear' }, ], ); UR::Object::Type->define( class_name => 'URT::GrandChild', is => 'URT::Child', has => [ name => { is => 'String', default_value => 'Doe' }, ], ); UR::Object::Type->define( class_name => 'URT::SingleChild', is => ['UR::Singleton', 'URT::Child'], ); UR::Object::Type->define( class_name =>'URT::BoolThing', has => [ boolval => { is => 'Boolean', default_value => 1 }, ], ); UR::Object::Type->define( class_name => 'URT::IntThing', has => [ intval => { is => 'Integer', default_value => 100 }, ], ); # Make a pair of classes we'll use to test setting indirect properties at # creation time. The ObjThing has an int_value, through a bridge, to an IntThing's intval UR::Object::Type->define( class_name => 'URT::BridgeThing', has => [ int_thing => { is => 'URT::IntThing', id_by => 'int_thing_id' }, int_value => { via => 'int_thing', to => 'intval' }, ], ); UR::Object::Type->define( class_name => 'URT::ObjThing', has => [ bridge_thing => { is => 'URT::BridgeThing', id_by => 'bridge_thing_id' }, int_value => { via => 'bridge_thing', to => 'int_value', default_value => 1234 }, ], ); UR::Object::Type->define( class_name => 'URT::CommandThing', is => 'Command', has => [ opt => { is => 'Boolean', default_value => 1 }, ], ); my $p = URT::Parent->create(id => 1); ok($p, 'Created a parent object without name'); is($p->name, 'Anonymous', 'object has default value for name'); is($p->name('Bob'), 'Bob', 'We can set the name'); is($p->name, 'Bob', 'And it returns the correct name after setting it'); $p = URT::Parent->create(id => 100, name => undef); ok($p, 'Created a parent object with the empty string for the name'); is($p->name, undef, 'Name is correctly empty'); is($p->name('Joe'), 'Joe', 'We can set it to something else'); is($p->name, 'Joe', 'And it returns the correct name after setting it'); my $o = URT::BoolThing->create(id => 1); ok($o, 'Created a BoolThing without a value'); is($o->boolval, 1, 'it has the default value for boolval'); is($o->boolval(0), 0, 'we can set the value'); is($o->boolval, 0, 'And it returns the correct value after setting it'); $o = URT::BoolThing->create(id => 2, boolval => 0); ok($o, 'Created a BoolThing with the value 0'); is($o->boolval, 0, 'it has the right value for boolval'); is($o->boolval(1), 1, 'we can set the value'); is($o->boolval, 1, 'And it returns the correct value after setting it'); $o = URT::IntThing->create(id => 1); ok($o, 'Created an IntThing without a value'); is($o->intval, 100, 'it has the default value for intval'); is($o->intval(1), 1, 'we can set the value'); is($o->intval, 1, 'And it returns the correct value after setting it'); $o = URT::IntThing->create(id => 2, intval => 0); ok($o, 'Created an IntThing with the value 0'); is($o->intval, 0, 'it has the right value for boolval'); is($o->intval(1), 1, 'we can set the value'); is($o->intval, 1, 'And it returns the correct value after setting it'); $o = URT::ObjThing->create(id => 1); ok($o, 'Created an ObjThing without an int_value'); is($o->int_value, 1234, 'It has the default value for int_value'); ok($o->bridge_thing_id, 'The ObjThing has a bridge_thing_id'); ok($o->bridge_thing, 'We can get its bridge_thing object'); is($o->bridge_thing->id, $o->bridge_thing_id, 'The IDs match for bridge_thing_id and URT::BridgeThing ID param'); $o = $o->bridge_thing; is($o->int_value, 1234, 'The BridgeThing has the correct value for int_value'); ok($o->int_thing, 'We can get its int_thing object'); is($o->int_thing->id, $o->int_thing_id, "The IDs match for the hangoff object"); is($o->int_thing->intval, 1234, "The int_thing's intval is 1234"); $o = URT::ObjThing->create(id => 2, int_value => 9876); ok($o, 'Created ObjThing with int_value 9876'); is($o->int_value, 9876, 'It has the correct value for int_value'); ok($o->bridge_thing_id, 'The ObjThing has a bridge_thing_id'); ok($o->bridge_thing, 'We can get its bridge_thing object'); is($o->bridge_thing->id, $o->bridge_thing_id, 'The IDs match for bridge_thing_id and URT::BridgeThing ID param'); $o = $o->bridge_thing; is($o->int_value, 9876, 'The BridgeThing has the correct value for int_value'); ok($o->int_thing_id, 'The BridgeThing has an int_thing_id value'); ok($o->int_thing, 'We can get its int_thing object'); is($o->int_thing->id, $o->int_thing_id, "The IDs match for the hangoff object"); is($o->int_thing->intval, 9876, "The int_thing's intval is 9876"); my $int_thing = URT::IntThing->get(intval => 1234); ok($int_thing, 'Got the IntThing with intval 1234, again'); $o = URT::ObjThing->create(id => 3); ok($o, 'Created another ObjThing without an int_value'); is($o->int_value, 1234, "The ObjThing's int_value is the default 1234"); ok($o->bridge_thing, "This ObjThing's bridge_thing property has a value"); is($o->bridge_thing->int_thing_id, $int_thing->id, 'The bridge_thing points to the original IntThing having the value 1234'); $p = URT::Parent->create(id => 2, name => 'Fred'); ok($p, 'Created a parent object with a name'); is($p->name, 'Fred', 'Returns the correct name'); my $c = URT::Child->create(); ok($c, 'Created a child object without name or color'); is($c->name, 'Anonymous', 'child has the default value for name'); is($c->color, 'clear', 'child has the default value for color'); is($c->name('Joe'), 'Joe', 'we can set the value for name'); is($c->name, 'Joe', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); $c = URT::GrandChild->create(); ok($c, 'Created a grandchild object without name or color'); is($c->name, 'Doe', 'child has the default value for name'); is($c->color, 'clear', 'child has the default value for color'); is($c->name('Joe'), 'Joe', 'we can set the value for name'); is($c->name, 'Joe', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); $c = URT::SingleChild->_singleton_object; ok($c, 'Got an object for the child singleton class'); is($c->name, 'Anonymous','name has the default value'); is($c->name('Mike'), 'Mike', 'we can set the name'); is($c->name, 'Mike', 'And it returns the correct name after setting it'); is($c->color, 'clear', 'color still returns the default value'); my $cmd = URT::CommandThing->create(); ok($cmd, 'Got a CommandThing object without specifying --opt'); is($cmd->opt, 1, '--opt value is 1'); $cmd = URT::CommandThing->create(opt => 0); ok($cmd, 'Created CommandThing with --opt 0'); is($cmd->opt, 0, '--opt value is 0'); # test oo defaults my $p1 = URT::Parent->get(1); my $p2 = URT::Parent->get(2); class URT::Thing2a { has => [ o1 => { is => 'URT::Parent', default_value => 2 }, ] }; class URT::Thing2b { has => [ o1 => { is => 'URT::Parent', id_by => 'o1_id', default_value => 2 }, ] }; class URT::Thing2c { has => [ o1 => { is => 'URT::Parent', is_many => 1, default_value => [1,2] }, ] }; note("test default values specified as IDs"); my $t1 = URT::Thing2a->create(); is($t1->o1, $p2, "default value is set (no id_by): $p2"); my $t2 = URT::Thing2b->create(); is($t1->o1, $p2, "default value is set (with id_by) $p2"); my $t3 = URT::Thing2c->create(); my @t3o1 = $t3->o1; is("@t3o1", "$p1 $p2", "default value is set to two items on an is_many property"); note("test default values overridden in construction not doing anything"); my $t4 = URT::Thing2a->create(o1 => $p1); is($t4->o1, $p1, "value is set as specified to $p1 not the default $p2"); my $t5 = URT::Thing2b->create(o1 => $p1); is($t5->o1, $p1, "value is set as specified to $p1 not the default $p2 (id_by)"); $DB::single = 1; my $t6 = URT::Thing2c->create(o1 => [$p2]); my @t6o1 = $t6->o1; is("@t6o1", "$p2", "value is set to as specified $p2 no the default of $p1 and $p2 (is_many)"); note("test default values specified as queries"); class URT::Thing3a { has => [ o1 => { is => 'URT::Parent', default_value => { name => "Fred" } }, ] }; class URT::Thing3b { has => [ o1 => { is => 'URT::Parent', id_by => 'o1_id', default_value => { name => "Fred" } }, ] }; class URT::Thing3c { has => [ o1 => { is => 'URT::Parent', is_many => 1, default_value => { name => ["Fred","Bob"] } }, ] }; my $t7 = URT::Thing3a->create(); is($t7->o1, $p2, "default value is $p2 as specified by query"); my $t2q = URT::Thing3b->create(); is($t7->o1, $p2, "default value is $p2 as specified by query"); my $t9 = URT::Thing3c->create(); my @t9o1 = $t9->o1; is("@t9o1", "$p1 $p2", "default value is set to both $p1 and $p2 as specified by query"); SKIP: { skip "UR::Command::sub_command_dirs() complains if there's no module, even if the class exists", 4; my($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv('--opt'); is($cmd_class, 'URT::CommandThing', 'resolved the correct command class'); is($params->{'opt'}, 1, 'Specifying --opt on the command line sets opt param to 1'); ($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv(); is($params->{'opt'}, 1, 'opt option has the default value with no argv arguments'); ($cmd_class,$params) = URT::CommandThing->resolve_class_and_params_for_argv('--noopt'); is($params->{'opt'}, 0, 'Specifying --noopt sets opt params to 0'); } 50_load_objects_that_stringify_false.t000444023532023421 213212121654174 22410 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 6; # This tests some fixes to the Context loading code that was being sloppy with # objects with stringify overloading. A simple boolean test on an object like # if ($object) {... # would stringify the object and then test that string for truthness. If the # string was "" or "0", then it would be boolean false package URT::Thing; use overload ( '""' => \&stringify ); UR::Object::Type->define( class_name => 'URT::Thing', is => 'UR::Value', ); sub stringify { return "" }; # always stringify to false package main; my $o = URT::Thing->get(1); ok(defined($o), 'Got Thing with id 1'); is($o->id, 1, 'It has the right ID'); $o = URT::Thing->get(0); ok(defined($o), 'Got Thing with id 0'); is($o->id, 0, 'It has the right ID'); my @o = URT::Thing->get([4,7,10,99,1]); is(scalar(@o), 5, 'Got 5 Things by ID'); is_deeply([map { $_->id} @o], [1,10,4,7,99], 'All the IDs were correct'); 87f_via_property_joins_to_itself.t000444023532023421 365612121654174 21661 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 5; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; # The 'father_name' property of Person requires a join back to Person # Now that we don't index delegated properties, we should be able to # load the whole result in one query $dbh->do('create table person (person_id integer primary key not null, name varchar, father_id integer references person(person_id))'); # Bob is Fred's father. Bob doesn't have a father recorded in the table $dbh->do("insert into person values (1,'Bob', null)"); $dbh->do("insert into person values (2,'Fred', 1)"); # Mike is Joe's father $dbh->do("insert into person values (3,'Mike', null)"); $dbh->do("insert into person values (4,'Joe', 3)"); # Bob (no relation to the first Bob) is Frank's father, and Bubba is Bob's father $dbh->do("insert into person values (5,'Bubba', null)"); $dbh->do("insert into person values (6,'Bob', 5)"); $dbh->do("insert into person values (7,'Frank', 6)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => 'person_id', has => [ name => { is => 'String' }, father => { is => 'Person', id_by => 'father_id' }, father_name => { via => 'father', to => 'name' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my @p = Person->get(father_name => 'Bob'); is(scalar(@p), 2, 'Got 2 people back'); is($p[0]->name, 'Fred', 'First is the right person'); is($p[1]->name, 'Frank', 'Second is the right person'); is($query_count, 1, 'Made one query'); 47_indirect_is_many_accessor.t000444023532023421 554612121654174 20714 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Data::Dumper; use Test::More; plan tests => 14; UR::Object::Type->define( class_name => 'URT::Param', id_by => [ thing_id => { is => 'Number' }, name => { is => 'String' }, value => { is => 'String'}, ], has => [ thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ 'thing_id' => { is => 'Number' }, ], has => [ params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1 }, # Actually, either of these property definitions will work interesting_param_values => { via => 'params', to => 'value', is_many => 1, is_mutable => 1, where => [ name => 'interesting'] }, bob_param_value => { via => 'params', to => 'value', where => [name => 'bob'] }, #interesting_params => { is => 'URT::Param', reverse_as => 'thing', is_many => 1, # where => [name => 'interesting']}, #interesting_param_values => { via => 'interesting_params', to => 'value', is_many => 1, is_mutable => 1 }, ], ); # make a non-interesting one ahead of time URT::Param->create(thing_id => 2, name => 'uninteresting', value => '123'); my $o = URT::Thing->create(thing_id => 2, interesting_param_values => ['abc','def']); ok($o, 'Created another Thing'); my @params = $o->params(); is(scalar(@params), 3, 'And it has 3 attached params'); isa_ok($params[0], 'URT::Param'); isa_ok($params[1], 'URT::Param'); isa_ok($params[2], 'URT::Param'); @params = sort { $a->value cmp $b->value } @params; is($params[0]->name, 'uninteresting', "param 1's name is uninteresting"); is($params[1]->name, 'interesting', "param 2's name is interesting"); is($params[2]->name, 'interesting', "param 3's name is interesting"); is($params[0]->value, '123', "param 1's value is correct"); is($params[1]->value, 'abc', "param 2's value is correct"); is($params[2]->value, 'def', "param 3's value is correct"); # Try to get the object again w/ id my $o2 = URT::Thing->get(2); ok($o2, 'Got thingy w/ id 2'); is_deeply([ $o->interesting_param_values ], [ $o2->interesting_param_values ], 'Ineresting values match those from orginal object'); my @o = URT::Thing->get(bob_param_value => undef); is(scalar(@o), 1, 'Got one thing back with no bob_param_value'); # Try to get the object again w/ id and ineresting values # FIXME does not work #my $o3 = URT::Thing->get( # thing_id => 2, # interesting_param_values => ['abc','def'], #); #ok($o3, 'Got thingy w/ id 2 and interesting_param_values => [qw/abc def/]'); #is_deeply([ $o->interesting_param_values ], [ $o3->interesting_param_values ], 'Ineresting values match those from original object'); 45_rollback_deleted_object.t000444023532023421 1112312121654174 20321 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 60; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeSQLite; # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); # SQLite's rollback un-does the table creation, too, so we # have to re-create the table and object when no-commit is on # And this needs to be re-created each time the main Context rolls back # because subscription creation is a transactional action my $init_db = sub { $dbh->do('create table IF NOT EXISTS person ( person_id int NOT NULL PRIMARY KEY, name varchar)'); $dbh->do(q(delete from person)); $dbh->do(q(insert into person (person_id, name) values (1, 'Bob'))); }; $init_db->(); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'person', id_by => [ 'person_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Person"); my $o = URT::Person->get(person_id => 1); ok($o, 'Got an object'); { my $within = sub { ok($o->delete, 'Object deleted ok'); isa_ok($o, 'UR::DeletedRef'); ok(! URT::Person->get(person_id => 1), 'get() does not return the deleted object'); }; my $after = sub { isa_ok($o, 'URT::Person'); my $o2 = URT::Person->get(person_id => 1); ok($o2, 'get() returns the object again'); is($o2, $o, 'the returned object is the same reference as the original'); }; &try_in_sw_transaction($within, $after); &try_in_context_transaction($within,$after); } { my $within = sub { ok($o->delete, 'Delete the object'); isa_ok($o, 'UR::DeletedRef'); my $new_o = URT::Person->create(person_id => 1, name => 'Fred'); ok($new_o, 'Created a new Person with the same ID as the deleted one'); is($new_o, $o, 'They are the same reference'); # The IDs are the same, so they're the same thing isa_ok($new_o, 'URT::Person'); is($new_o->name, 'Fred', 'Name is the new object name'); }; my $after = sub { isa_ok($o, 'URT::Person'); my $o2 = URT::Person->get(person_id => 1); ok($o2, 'get() returns the object again'); is($o2, $o, 'the returned object is the same reference as the original'); is($o->name, 'Bob', 'Name is the original object name'); }; &try_in_sw_transaction($within, $after); &try_in_context_transaction($within,$after); } { # Doing this with the outer Context's transaction makes no sense # Just test in a software transaction my $trans1 = UR::Context::Transaction->begin(); ok($trans1, 'Started a software transaction'); ok($o->name('Fred'), 'Change object name to Fred'); my $trans2 = UR::Context::Transaction->begin(); ok($trans2,'Start an inner transaction'); ok($o->delete,'Delete the object'); isa_ok($o, 'UR::DeletedRef'); ok(! URT::Person->get(person_id => 1), 'get() does not return the deleted object'); ok($trans2->rollback, 'Rollback inner transaction'); isa_ok($o, 'URT::Person'); is($o->name, 'Fred', 'Object name is still Fred'); ok($trans1->rollback, 'Rollback outter transaction'); is($o->name, 'Bob', 'Object name is back to Bob'); } { # And this one makes no sense with a software transaction since # it needs to hit the DB ok(UR::DBI->no_commit(1), 'Turn on no-commit'); my $new_o = URT::Person->create(person_id => 2, name => 'Fred'); ok($new_o, 'Create a new Person'); ok(UR::Context->commit(),'Context commit'); ok($new_o->delete(),'Delete the new object'); isa_ok($new_o, 'UR::DeletedRef'); ok(UR::Context->rollback(),'Context rollback'); isa_ok($new_o, 'URT::Person'); is($new_o->name, 'Fred', 'The object name is Fred'); } #################################################################3 sub try_in_sw_transaction { my $within = shift; my $after = shift; my $trans = UR::Context::Transaction->begin(); ok($trans, 'Started a software transaction'); $within->(); ok($trans->rollback(), 'rollback the software transaction'); $after->(); } sub try_in_context_transaction { my $within = shift; my $after = shift; $within->(); ok(UR::Context->rollback(), 'rollback the context'); $init_db->(); $after->(); } 29b_join_calculated_accessor.t000444023532023421 577512121654174 20662 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; # Tests a get() with a delegated property, where the delegation is resolved via a # calculated property ok(setup(), 'Create initial schema, data and classes'); my $emp = URT::Employee->get(1); ok($emp, 'Got employee 1'); my $boss = $emp->boss; ok($boss, 'Got boss for employee 1'); my @emp = URT::Employee->get(company => 'CoolCo'); is(scalar(@emp), 2, 'Got 2 employees of CoolCo'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok($dbh->do('create table BOSS (boss_id int, first_name varchar, last_name varchar, company varchar)'), 'create table BOSS'); ok($dbh->do('create table EMPLOYEE (emp_id int, name varchar, is_secret, int, boss_id int CONSTRAINT boss_fk references BOSS(BOSS_ID))'), 'create table EMPLOYEE'); my $sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company) values (?,?,?,?)'); $sth->execute(1, 'Bob', 'Smith', 'CoolCo'); $sth->execute(2, 'Robert', 'Jones', 'Data Inc'); $sth->finish(); $sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)'); $sth->execute(1,'Joe', 1, 0); $sth->execute(2,'James', 1, 0); $sth->execute(3,'Jack', 2, 1); $sth->execute(4,'Jim', 2, 0); $sth->execute(5,'Jacob', 2, 1); $sth->finish(); ok($dbh->commit(), 'Commit records to DB'); # Bosses are pretty normal UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, company => { type => "String" }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); # An employee's boss is connected through the calculated property calc_boss_id UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, calc_boss_id => { calculate => q( return $self->boss_id ) }, # silly, but it's still a calculation boss => { type => "URT::Boss", id_by => 'calc_boss_id' }, company => { via => 'boss' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } 11d_create_with_single_delegated_property_via_is_many_property.t000444023532023421 410512121654174 27756 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More tests => 7; # classes class Person::Relationship { is => 'UR::Object', id_by => [ person_id => { is => 'Number', implied_by => 'person', }, related_id => { is => 'Number', implied_by => 'related' }, name => { is => 'Text', }, ], has => [ person => { is => 'Person', id_by => 'person_id', }, related => { is => 'Person', id_by => 'related_id' }, ], }; class Person { is => 'UR::Object', has => [ name => { is => 'Text', doc => 'Name of the person', }, relationships => { is => 'Person::Relationship', is_many => 1, is_optional => 1, reverse_as => 'person', doc => 'This person\'s relationships', }, friends => { is => 'Person', is_many => 1, is_optional => 1, is_mutable => 1, via => 'relationships', to => 'related', where => [ name => 'friend' ], doc => 'Friends of this person', }, best_friend => { is => 'Person', is_optional => 1, is_mutable => 1, via => 'relationships', to => 'related', where => [ name => 'best friend' ], doc => 'Best friend of this person', }, ], }; my $ronnie = Person->create( name => 'Ronald Reagan', ); ok($ronnie, 'created Ronnie'); is_deeply([$ronnie->friends], [], 'Ronnie does not have friends'); ok(!$ronnie->best_friend, 'Ronnie does not have a best friend'); # Create George my $bill = Person->create( name => 'Bill Clinton', friends => [$ronnie], #works ); is_deeply([$bill->friends], [$ronnie], 'Bill has friend(s)'); my $george = Person->create( name => 'George HW Bush', friends => [$ronnie], best_friend => $bill, #does not work ); ok($george, 'created George'); is_deeply([$george->friends], [$ronnie], 'George has friend(s)'); is_deeply($george->best_friend, $bill, 'George is best friends w/ bill'); 99_transaction-failed_commit_rollback.t000444023532023421 362612121654174 22506 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use UR; use IO::File; use Test::More tests => 11; UR::Object::Type->define( class_name => 'Circle', has => [ radius => { is => 'Number', default_value => 1, }, ], ); # Create a Circle my $circle = Circle->create(); ok($circle->isa('Circle'), 'create a circle'); ok($circle->radius == 1, 'default radius is 1'); { my $transaction = UR::Context::Transaction->begin; isa_ok($transaction, 'UR::Context::Transaction'); my $old_radius = $circle->radius; my $new_radius = $circle->radius + 5; isnt($circle->radius, $new_radius, "new circle radius isn't current radius"); $circle->radius($new_radius); is($circle->radius, $new_radius, "circle radius changed to new radius"); *Circle::__errors__ = sub { my $tag = UR::Object::Tag->create ( type => 'invalid', properties => ['test_property'], desc => 'intentional error for test', ); return ($tag); }; $transaction->dump_error_messages(0); $transaction->queue_error_messages(1); is($transaction->commit, undef, 'commit failed'); my @messages = $transaction->error_messages(); is(scalar(@messages), 2, 'commit generated 2 error messages'); my $circleid = $circle->id; is($messages[0], 'Invalid data for save!', 'First error text is correct'); #like($msgobj[1]->text, # qr(Circle identified by $circleid has problem on\nINVALID: property 'test_property': intentional error for test)m, # 'Error message text is correct'); like($messages[1], qr(Circle identified by $circleid has problems on\s+INVALID: property 'test_property': intentional error for test), 'Error message text is correct'); is($transaction->rollback, 1, 'rollback succeeded'); is($circle->radius, $old_radius, 'circle radius was rolled back due to forced __errors__'); } 1; 63c_view_with_subviews.t.expected.person.xml000444023532023421 374712121654174 23514 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t 111 Person Fester 99 222 Cat fluffy 2 11 111 Person 333 Cat nestor 8 22 111 Person 87c_query_by_is_many_indirect_is_efficient.t000444023532023421 1270312121654174 23640 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 20; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color', is_many => 1 }, car_parts => { is => 'URT::CarParts', via => 'cars', to => 'parts', is_optional => 1, is_many => 1 }, car_parts_prices => { via => 'cars', to => 'parts_prices', is_optional => 1, is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, parts_prices => { via => 'parts', to => 'price', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1], [6, 'Hobo Cliff', 0] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my @people = URT::Person->get(car_color => 'pink'); is(scalar(@people), 0, 'No person has a pink car'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @people = URT::Person->get(car_color => 'red'); is(scalar(@people), 2, '2 people have red cars'); is($query_count, 1, 'Made 1 query'); is($people[0]->name, 'Bob', 'Bob is the first person returned'); is($people[1]->name, 'Mike', 'Mike is the second person returned'); $query_count = 0; my @cars = URT::Car->get(owner_id => $people[1]->id, color => 'red'); is(scalar(@cars), 1, 'Mike has 1 red car'); is($query_count, 0, 'Made no queries'); $query_count = 0; @cars = URT::Car->get(owner_id => $people[0]->id); is(scalar(@cars), 2, 'Bob has 2 cars'); is($query_count, 1, 'Made 1 query'); # Needed to query since the first via URT::Person only loaded red cars $query_count = 0; @people = URT::Person->get(is_cool => 0, -hints => ['cars']); is(scalar(@people), 3, "got three people, with a hint to get their cars, when one has no cars"); for my $person (@people) { my @cars = $person->cars(); note("person $person has " . scalar(@cars) . " cars"); } is($query_count, 1, 'Made 1 query. The hints loaded all the related cars'); 63c_view_with_subviews.t.expected.cat_set.text000444023532023421 13412121654174 23757 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tAcme Cat Set Acme::Cat/And/owner_id/O:O:111 members: Acme Cat 222 Acme Cat 333 45_deleted_subclassed_objects_stay_deleted.t000444023532023421 361512121654174 23560 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 11; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); { my $o = URT::Parent->get(parent_id => 1); ok($o, 'Got an object'); isa_ok($o, 'URT::Parent'); isa_ok($o, 'URT::Child'); ok($o->delete, 'Object deleted ok'); } { my $o = URT::Parent->get(parent_id => 1); ok(! $o, 'get() with the deleted ID returns nothing'); } { my $o = URT::Parent->get(parent_id => 1); ok(! $o, 'get() with the deleted ID again returns nothing'); } unlink(URT::DataSource::SomeSQLite->server); # Remove the file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', is => [ 'URT::Parent' ], ), "Created class for Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name) values (1, 'Bob'))), "insert a parent object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return 'URT::Child'; } 02_class_construction.t000444023532023421 767112121654174 17421 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 24; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $c1 = UR::Object::Type->define(class_name => 'URT::Foo', data_source => "URT::DataSource::SomeSQLite", table_name => "FOO"); is($URT::Foo::ISA[0], 'UR::Entity', "defined class has correct inheritance"); is($URT::Foo::Type::ISA[0], 'UR::Entity::Type', "defined class' meta class has correct inheritance"); my $c1b = UR::Object::Type->get(data_source_id => "URT::DataSource::SomeSQLite", table_name => "FOO"); is($c1b,$c1, "defined class is gettable"); my $c2 = UR::Object::Type->create(class_name => 'URT::Bar', data_source => "URT::DataSource::SomeSQLite", table_name => "BAR"); is($URT::Bar::ISA[0], 'UR::Entity', "created class has correct inheritance"); is($URT::Bar::Type::ISA[0], 'UR::Entity::Type', "created class' meta class has correct inheritance"); my $c2b = UR::Object::Type->get(data_source_id => "URT::DataSource::SomeSQLite", table_name => "BAR"); is($c2b,$c2, "created class is gettable"); my $c3_parent = UR::Object::Type->define( class_name => 'URT::BazParent', id_by => ['id_prop_a','id_prop_b'], has => [ id_prop_a => { is => 'Integer' }, id_prop_b => { is => 'String' }, prop_c => { is => 'Number' }, ], ); ok($c3_parent, 'Created a parent class'); is($URT::BazParent::ISA[0], 'UR::Object', 'defined class has correct inheritance'); is($URT::BazParent::Type::ISA[0], 'UR::Object::Type', "defined class' meta class has correct inheritance"); my %props = map { $_->property_name => $_ } $c3_parent->properties; is(scalar(keys %props), 4, 'Parent class property count correct'); is($props{'id_prop_a'}->is_id, '0 but true', 'id_prop_a is an ID property and has the correct rank'); is($props{'id_prop_b'}->is_id, '1', 'id_prop_b is an ID property and has the correct rank'); is($props{'prop_c'}->is_id, undef, 'prop_c is not an ID property'); my %id_props = map { $_->property_name => 1 } $c3_parent->id_properties; is(scalar(keys %id_props), 3, 'Parent class id property count correct'); is_deeply(\%id_props, { id_prop_a => 1, id_prop_b => 1, id => 1 }, 'all ID properties are there'); my $c3 = UR::Object::Type->define( class_name => 'URT::Baz', is => 'URT::BazParent', has => [ prop_d => { is => 'Number' }, ], ); ok($c3, 'Created class with some properties and a parent class'); is($URT::Baz::ISA[0], 'URT::BazParent', 'defined class has correct inheritance'); is($URT::Baz::Type::ISA[0], 'URT::BazParent::Type', "defined class' meta class has correct inheritance"); %props = map { $_->property_name => $_ } $c3->properties; is(scalar(keys %props), 5, 'property count correct'); is($props{'id_prop_a'}->is_id, '0 but true', 'id_prop_a is an ID property and has the correct rank'); is($props{'id_prop_b'}->is_id, '1', 'id_prop_b is an ID property and has the correct rank'); is($props{'prop_c'}->is_id, undef, 'prop_c is not an ID property'); is($props{'prop_d'}->is_id, undef, 'prop_d is not an ID property'); my $other_class = UR::Object::Type->define( class_name => 'URT::OtherClass', id_by => [ id => { is => 'String' }, ], ); my $parent_with_id_prop = UR::Object::Type->define( class_name => 'URT::ParentWithProp', has => [ other_id => { is => 'Integer' }, ], ); $DB::foo=1; my $child_without_id_prop = UR::Object::Type->define( class_name => 'URT::ChildWithoutProp', is => 'URT::ParentWithProp', has => [ other => { is => 'URT::OtherClass', id_by => 'other_id' } ], ); is($child_without_id_prop->property_meta_for_name('other_id')->data_type, 'Integer', 'implied property gets data_type from parent when specified'); 41_rpc_basic.t000444023532023421 1442512121654174 15445 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More; plan tests => 40; use File::Basename; BEGIN { use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; } use URT; use IO::Socket; ok(UR::Object::Type->define( class_name => 'URT::RPC::Thingy', is => 'UR::Service::RPC::Executer'), 'Created class for RPC executor'); my($to_server,$to_client) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC); ok($to_server, 'Created socket'); ok($to_client, 'Created socket'); my $rpc_executer = URT::RPC::Thingy->create(fh => $to_client); my $rpc_server = UR::Service::RPC::Server->create(); ok($rpc_server, 'Created an RPC server'); ok($rpc_server->add_executer($rpc_executer), 'Added the executer to the server'); #my $rpc_client = UR::Service::RPC::Client->create(fh => $to_server); #ok($rpc_client, 'Created an RPC client'); my $count = $rpc_server->loop(1); is($count, 0, 'RPC server ran the event loop and correctly processed 0 events'); my $retval; my @join_args = ('one','two','three','four'); my $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); ok($msg, 'Created an RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); do { local *STDERR; no warnings; $count = $rpc_server->loop(1); use warnings; }; is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); is($URT::RPC::Thingy::join_called, 1, 'RPC server called the correct method'); my $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my $expected_return_value = join('-',@join_args); my @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], $expected_return_value, 'Response return value is correct'); is($resp->exception, undef, 'Response correctly has no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'illegal', params => \@join_args, 'wantarray' => 0, ); ok($msg, 'Created another RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); is($URT::RPC::Thingy::exception, 1, 'RPC server correctly rejected the method call'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); is($resp->return_value, undef, 'Response return value is correctly empty'); is($resp->exception, 'Not allowed', 'Response excpetion is correctly set'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'some_undefined_function', params => [], 'wantarray' => 0, ); ok($msg, 'Created third RPC message encoding an undefined function call'); ok($msg->send($to_server), 'Sent RPC message from client'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); ok($resp->exception =~ m/(Can't locate object method|Undefined sub).*some_undefined_function/, 'Response excpetion correctly reflects calling an undefined function'); my $string = 'a string with some words'; my $pattern = '(\w+) (\w+) (\w+)'; my $regex = qr($pattern); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 0, ); ok($msg, 'Created RPC message for match in scalar context'); ok($msg->send($to_server), 'Sent RPC message to server'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], 1, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 1, ); ok($msg, 'Created RPC message for match in list context'); ok($msg->send($to_server), 'Sent RPC message to server'); $count = $rpc_server->loop(1); is($count, 1, 'RPC server ran the event loop and correctly processed 1 event'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my @expected_return_value = qw(a string with); @return_values = $resp->return_value_list; is_deeply(\@return_values, \@expected_return_value, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); # END of the main script package URT::RPC::Thingy; sub authenticate { my($self,$msg) = @_; if ($msg->method_name eq 'illegal') { $URT::RPC::Thingy::exception++; $msg->exception('Not allowed'); return; } else { return 1; } } sub join { my($joiner,@args) = @_; $URT::RPC::Thingy::join_called++; my $string = join($joiner, @args); return $string; } # A thing that will return different values in scalar and list context sub match { my($string, $regex) = @_; # my $pattern = qr($pattern); return $string =~ $regex; } 63_view_text.t000444023532023421 574512121654174 15527 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; eval "use XML::LibXML"; eval "use XML::LibXSLT"; my $TEST_XML = 1; unless ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'}) { $TEST_XML = undef; } use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use above 'UR'; class Animal { has => [ name => { is => 'Text' }, age => { is => 'Number' }, ] }; class Person { is => 'Animal', has => [ cats => { is => 'Cat', is_many => 1, reverse_as => 'owner' }, favorite_numbers => { is_many => 1 }, ] }; class Cat { is => 'Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Person', id_by => 'owner_id' }, buddy => { is => 'Cat', id_by => 'buddy_id', is_optional => 1 }, ] }; my $p = Person->create(id => 1001, name => 'Fester', age => 99, favorite_numbers => [2,4,7]); ok($p, "made a test person object to have cats"); my $c1 = Cat->create(id => 2001, name => 'fluffy', age => 2, owner => $p, fluf => 11); ok($c1, "made a test cat 1"); my $c2 = Cat->create(id => 2002, name => 'nestor', age => 8, owner => $p, fluf => 22, buddy => $c1); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); ######### my @toolkits = $TEST_XML ? ( 'xml','text' ) : ( 'text' ); for my $toolkit (@toolkits) { note('view 1: no aspects'); my $pv1 = $p->create_view( toolkit => $toolkit, aspects => [ ] ); ok($pv1, "got an XML view $pv1 for the object $p"); my @a = $pv1->aspects(); is(scalar(@a),0,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); my @an = $pv1->aspect_names(); is("@an","","got expected aspect list @an"); ######### note('view 2: simple aspects'); my $pv2 = $p->create_view( toolkit => $toolkit, aspects => [ 'name', 'age', 'cats', ] ); ok($pv2, "got an XML view $pv2 for the object $p"); @a = $pv2->aspects(); is(scalar(@a),3,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); @an = $pv2->aspect_names(); is("@an","name age cats","got expected aspect list @an"); ######### note('view 3: aspects with properties'); my $pv3 = $p->create_view( toolkit => $toolkit, aspects => [ { name => 'name', label => 'NAME' }, 'age', { name => 'cats', label => 'Kitties', }, ] ); ok($pv3, "got an XML view $pv3 for the object $p"); @a = $pv3->aspects(); is(scalar(@a),3,"got expected aspect list @a") or diag(Data::Dumper::Dumper(@a)); @an = $pv3->aspect_names(); is("@an","name age cats","got expected aspect list @an"); my $s = $pv3->subject; is($s, $p, "subject is the original model object"); #$pv3->show; my $c = $pv3->content; note($c); } done_testing(); 51_get_with_hints.t000444023532023421 1544412121654174 16542 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 50; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #UR::DBI->monitor_sql(1); my $query_count; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created subscription to count queries'); #$DB::single = 1; $query_count = 0; my $thing = URT::Thing->get(name => 'Bob', -hints => [ 'attribs' ]); ok($thing, 'get() returned an object'); is($thing->name, 'Bob', 'object name is correct'); is($thing->thing_id, 1, 'ID is correct'); is($query_count, 1, 'Correctly made 1 query'); $query_count = 0; my @attribs = URT::Attrib->is_loaded(); is(scalar(@attribs), 2, 'The last get() also loaded 2 attribs'); @attribs = sort { $a->attrib_id <=> $b->attrib_id } @attribs; # Just in case, but they should already be in this order... is($query_count, 0, 'Correctly made no queries'); is($attribs[0]->name, 'alignment', 'First attrib name is correct'); is($attribs[0]->value, 'good', 'First attrib value is correct'); is($attribs[1]->name, 'job', 'Second attrib name is correct'); is($attribs[1]->value, 'cook', 'Second attrib value is correct'); $query_count = 0; @attribs = $thing->attribs(); is(scalar(@attribs), 2, 'accessing attribs through the delegated property returned 2 things'); is($query_count, 0, 'Correctly made no queries'); is($attribs[0]->name, 'alignment', 'First attrib name is correct'); is($attribs[0]->value, 'good', 'First attrib value is correct'); is($attribs[1]->name, 'job', 'Second attrib name is correct'); is($attribs[1]->value, 'cook', 'Second attrib value is correct'); $query_count = 0; my $person = URT::Person->get(name => 'Frank', -hints => ['params']); ok($person, 'get() returned an object'); is($person->name, 'Frank', 'object name is correct'); is($person->person_id, 2, 'ID is correct'); is($query_count, 1, 'Correctly made 1 query'); my @bridges = URT::Bridge->is_loaded(); is(scalar(@bridges), 3, '3 bridges were loaded from the above query'); my @params = URT::Param->is_loaded(); is(scalar(@params), 3, '3 params were loaded from the above query'); $query_count = 0; @bridges = $person->bridges(); is(scalar(@bridges), 3, 'got 3 bridges through the delegated accessor'); is($query_count, 0, 'Correctly made no queries'); $query_count = 0; @params = $person->params(); is(scalar(@params), 3, 'got 3 params through the delegated accessor'); is($query_count, 0, 'Correctly made no queries'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # attribs belong to one thing ok( $dbh->do("create table thing (thing_id integer, name varchar)"), 'Created thing table'); ok( $dbh->do("create table attrib (attrib_id integer, name varchar, value varchar, thing_id integer REFERENCES thing(thing_id))"), 'Created attrib table'); my $insert = $dbh->prepare("insert into thing (thing_id, name) values (?,?)"); foreach my $row ( ( [1, 'Bob'], [2, 'Christine']) ) { ok( $insert->execute(@$row), 'Inserted a thing'); } $insert->finish; $insert = $dbh->prepare("insert into attrib (attrib_id, name, value, thing_id) values (?,?,?,?)"); foreach my $row ( ( [1, 'alignment', 'good', 1], [2, 'job', 'cook', 1], [3, 'alignment', 'evil', 2], [4, 'color', 'red', 2] ) ) { ok($insert->execute(@$row), 'Inserted an attrib'); } $insert->finish; # params are many-to-many with people ok( $dbh->do("create table person (person_id integer, name varchar)"), 'created table foo'); ok( $dbh->do("create table param (param_id integer, name varchar, value varchar)"), 'created param table'); ok( $dbh->do("create table person_param_bridge (person_id integer REFERENCES person(person_id), param_id integer REFERENCES param(param_id), PRIMARY KEY (person_id, param_id))" ), 'created bridge table'); $insert = $dbh->prepare("insert into person (person_id, name) values (?,?)"); foreach my $row ( ( [ 1, 'Joe'], [ 2, 'Frank'] )) { ok($insert->execute(@$row), 'inserted a person'); } $insert->finish; $insert = $dbh->prepare("insert into param (param_id, name, value) values (?,?,?)"); foreach my $row ( ( [ 1, 'rank', 'cog' ], [ 2, 'status', 'single' ], [ 3, 'title', 'capn' ], [ 4, 'tag', 'xyzzy' ] )) { ok($insert->execute(@$row), 'inserted a param'); } $insert->finish; $insert = $dbh->prepare("insert into person_param_bridge (person_id, param_id) values (?,?)"); foreach my $row ( ( [ 1, 1 ], [ 2, 1 ], [ 2, 2 ], [ 2, 4 ] )) { ok($insert->execute(@$row), 'inserted a bridge'); } $insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, attribs => { is => 'URT::Attrib', reverse_as => 'thing', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Attrib', id_by => 'attrib_id', has => [ name => { is => 'String' }, value => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'attrib', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => 'name', has_many_optional => [ bridges => { is => 'URT::Bridge', reverse_as => 'persons' }, params => { via => 'bridges', to => 'params' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Param', id_by => 'param_id', has => ['name','value'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'param', ); UR::Object::Type->define( class_name => 'URT::Bridge', id_by => [ 'person_id', 'param_id' ], has => [ persons => { is => 'URT::Person', id_by => 'person_id' }, params => { is => 'URT::Param', id_by => 'param_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person_param_bridge', ); } 63c_view_with_subviews.t000444023532023421 1142712121654174 17622 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; # let the developer supply the toolkits to test # or default to all of them # TODO: add html when it stops dying our @toolkits = @ARGV; eval "use XML::LibXML"; eval "use XML::LibXSLT"; my $TEST_XML = 1; unless ($INC{"XML/LibXML.pm"} && $INC{'XML/LibXSLT.pm'}) { $TEST_XML = undef; } use UR; unless (@toolkits) { @toolkits = $TEST_XML ? qw/json xml text/ : qw/json text/; } class Acme { is => 'UR::Namespace' }; ## value data type class Acme::Value::Years { is => 'UR::Value::Number', }; sub Acme::Value::Years::__display_name__ { my $self = shift; return $self->id . ' yrs'; }; ## value data types can be gotten by their identity ## they cannot be created, deleted, or mutated my $age1 = Acme::Value::Years->get(88); is($age1->__display_name__, "88 yrs", "$age1 has id " . $age1->id . " and display name " . $age1->id . " yrs"); my $age2 = Acme::Value::Years->get(22); is($age2->__display_name__, "22 yrs", "$age2 has id " . $age2->id . " and display name " . $age2->id . " yrs"); ## entity data types class Acme::Animal { has => [ name => { is => 'Text' }, age => { is => 'Years' }, ] }; class Acme::Person { is => 'Acme::Animal', has => [ cats => { is => 'Acme::Cat', is_many => 1 }, ] }; class Acme::Cat { is => 'Acme::Animal', has => [ fluf => { is => 'Number' }, owner => { is => 'Acme::Person', id_by => 'owner_id' }, owner_age => { is => 'Number', via => 'owner', to => 'age' }, ] }; ## the set of entities of a given set is finite, and can be created, mutated, deleted my $p = Acme::Person->create(name => 'Fester', age => 99, id => 111); ok($p, "made a test person object to have cats"); my $c1 = Acme::Cat->create(name => 'fluffy', age => 2, owner => $p, fluf => 11, id => 222); ok($c1, "made a test cat 1"); my $c2 = Acme::Cat->create(name => 'nestor', age => 8, owner => $p, fluf => 22, id => 333); ok($c2, "made a test cat 2"); my @c = $p->cats(); is("@c","$c1 $c2", "got expected cat list for the owner"); $DB::single = 1; my $cat_set = $p->cat_set(); ok($cat_set, "got a set object representing the test person's set of cats: $cat_set"); ## as we render the person and the cat set, we will show the same aspects for each class my @person_aspects = ( 'name', 'age', { name => 'cats', #perspective => 'default', #toolkit => 'text', aspects => [ 'name', 'age', 'fluf', 'owner' ], } ); my @cat_set_aspects = ( 'id', 'members', #'owner', #'owner_age', ); # render both objects, in a variety of text-based views for my $obj_aspects_pair ( [$p,\@person_aspects], [$cat_set,\@cat_set_aspects] ) { my ($obj, $aspects) = @$obj_aspects_pair; for my $toolkit (@toolkits) { # TODO: add 'html' to this list note("\nVIEW: " . ref($obj) . " as $toolkit...\n \n"); for my $aspect (@$aspects) { if (ref($aspect) eq 'HASH') { $aspect->{toolkit} = $toolkit; $aspect->{perspective} = 'default'; } } my $view = $obj->create_view( toolkit => $toolkit, aspects => $aspects, ); ok($view, "got an text view for the person"); $DB::single = 1; my $actual_content = $view->content; ok($actual_content, "$toolkit view of " . ref($obj) . " generated content"); my $expected_content_path = ref($obj); $expected_content_path =~ s/Acme:://; $expected_content_path =~ s/::/_/g; $expected_content_path = lc($expected_content_path); $expected_content_path = __FILE__ . '.expected.' . $expected_content_path . '.' . $toolkit; # this will cause us to skip missing toolkits w/o failing for now. # when json, xml and html all work remove these 4 lines... unless (-e $expected_content_path) { note("No file at $expected_content_path. Cannot validate:\n$actual_content"); next; }; # this is the _actual_ test for above when all tookits are in place ok(-e $expected_content_path, "path exists to expected content for toolkit $toolkit") or do { diag("No file at $expected_content_path? Cannot validate:\n$actual_content"); next; }; my $expected_content = join('', IO::File->new($expected_content_path)->getlines()); is($actual_content, $expected_content, "content matches!") or eval { # stage a file for debugging, or to upgrade the test IO::File->new(">$expected_content_path.new")->print($actual_content) }; #and note("WORKS ON:\n$actual_content"); } } done_testing(); 49i_complicated_get_join_through_value_class.t000444023532023421 517512121654174 24146 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 3; # tests a get() where the delegated property's join chain has a UR::Value class in the middle # # Before the fix, the QueryPlan would see that UR::Values are not resolvable in the DB, and so # stops trying to connect joins together, leading to multiple queries. The fix was to splice # out these non-db joins while constructing the SQL my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer not null primary key, name varchar not null)'); $dbh->do('create table attribute (attr_id integer not null primary key, person_id integer references person(person_id), key varchar, value varchar)'); $dbh->do('create table car (car_id integer not null primary key, make varchar, model varchar)'); $dbh->do("insert into person values (1,'Bob')"); $dbh->do("insert into car values (2,'Chevrolet','Impala')"); $dbh->do("insert into attribute values (3,1,'car_id', 2)"); UR::Object::Type->define( class_name => 'Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String', }, attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, car_id => { is => 'Integer', via => 'attributes', to => 'value', where => [key => 'car_id'] }, car => { is => 'Car', id_by => 'car_id' }, car_make => { is => 'String', via => 'car', to => 'make' }, ], ); UR::Object::Type->define( class_name => 'Attribute', data_source => 'URT::DataSource::SomeSQLite', table_name => 'attribute', id_by => [ attr_id => { is => 'Integer' }, ], has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String', }, value => { is => 'String', }, ], ); UR::Object::Type->define( class_name => 'Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ make => { is => 'String' }, model => { is => 'String' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); my $p = Person->get(car_make => 'Chevrolet'); ok($p, 'Got the person'); is($query_count, 1, 'Made one query'); 62b_in_not_in_operator.t000444023532023421 265412121654174 17535 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #test quote escaping in IN clauses my @odd_things = URT::Thing->get(value => [map(join("'", $_, $_), (1,3,5,7))]); is(scalar(@odd_things), 4, 'got back four objects'); my @even_things = URT::Thing->get('value not in' => [map(join("'", $_, $_), (1,3,5,7))]); is(scalar(@even_things), 4, 'got back four objects'); my %everything; for my $t (@odd_things, @even_things) { $everything{$t->id} = $t; } is(scalar(keys(%everything)), 8, 'got entire set of things betwixt the odd and even'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value varchar)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); foreach my $val ( 1,2,3,4,5,6,7,8 ) { $sth->execute($val,$val . "'" . $val); } $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 21e_old_subscription_api.t000444023532023421 774712121654174 20067 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 28; package URT::Person; UR::Object::Type->define( class_name => 'URT::Person', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], ); sub validate_subscription { my($class,$method) = @_; return 1 if $method eq 'something_else'; return $class->SUPER::validate_subscription($method); } package main; my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $change_count = get_change_count(); my $observations = {}; $p1->last_name("DoDo"); is_deeply($observations, {}, "no callback count change with no observers defined"); is(get_change_count(), $change_count + 1, '1 change recorded even with no observers'); foreach my $thing ( $p1,$p2,'URT::Person') { foreach my $aspect ( '','last_name','something_else' ) { my $id = ref($thing) ? $thing->id : $thing; my %args = ( callback => sub { no warnings 'uninitialized'; $observations->{$id}->{$aspect}++ } ); if ($aspect) { $args{'method'} = $aspect; } #ok($thing->add_observer(%args), "Made an observer on $thing for aspect $aspect"); ok($thing->create_subscription(%args), "Made an observer on $thing for aspect $aspect"); } } $change_count = get_change_count(); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply($observations, { 1 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; is($p2->last_name("Do"),"Do", "changed person 2"); is_deeply($observations, { 2 => { '' => 1, 'last_name' => 1 }, 'URT::Person' => { '' => 1, 'last_name' => 1 }, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, '1 change recorded'); $change_count = get_change_count(); $observations = {}; ok($p2->__signal_change__('something_else'),'send the "something_else" signal to person 2'); is_deeply($observations, { 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count + 1, 'one change recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_change__('something_else'), 'Send the "something_else" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1, 'something_else' => 1}, 2 => { '' => 1, 'something_else' => 1}, 'URT::Person' => { '' => 1, 'something_else' => 1}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); $change_count = get_change_count(); $observations = {}; ok(URT::Person->__signal_change__('blablah'), 'Send the "blahblah" signal to the URT::Person class'); is_deeply($observations, { 1 => { '' => 1,}, 2 => { '' => 1,}, 'URT::Person' => { '' => 1,}, }, 'Callbacks were fired'); is(get_change_count(), $change_count, 'no changes recorded for non-change signal'); sub get_change_count { my @c = map { scalar($_->__changes__) } URT::Person->get; my $sum = 0; do {$sum += $_ } foreach (@c); return $sum; } 17b_mk_rw_accessor_signals_property_change.t000444023532023421 126012121654174 23630 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use above 'UR'; use Test::More; package Car; class Car { has => [ make => { is => 'Text', }, ], }; sub make { my $self = shift; if (@_) { my $value = shift; $self->__make($value); } return $self->__make; } package main; my $car = Car->create(make => 'GM'); isa_ok($car, 'Car'); my $observer_ran = 0; $car->add_observer( aspect => 'make', callback => sub { $observer_ran = 1 }, ); is($observer_ran, 0, 'observer has not run yet'); $car->make('Ford'); is($car->make, 'Ford', 'make changed to Ford'); is($observer_ran, 1, 'observer triggered from make change'); done_testing(); 04c_postresql.t000444023532023421 66212121654174 15654 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring PostgreSQL"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomePostgreSQL->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 13b_dump_message_inheritance.t000444023532023421 2204412121654174 20677 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tBEGIN { # This environment variable overrides default UR behavior so unsetting it # for the test. delete $ENV{UR_COMMAND_DUMP_DEBUG_MESSAGES}; }; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More; plan tests => 142; ok(UR::Object::Type->define( class_name => 'A'), 'Define class A'); ok(UR::Object::Type->define( class_name => 'B'), 'Define class B'); my $a = A->create(id => 1); ok($a, 'Create object a'); my $b = B->create(id => 1); ok($b, 'Create object b'); # Make sure each instance can control its own messaging flags is($a->dump_debug_messages(0), 0, 'Set dump_debug_messages on a to 0'); is($a->dump_debug_messages(), 0, 'dump_debug_messages on a is still 0'); is($b->dump_debug_messages(1), 1, 'Set dump_debug_messages on b to 1'); is($b->dump_debug_messages(), 1, 'dump_debug_messages on b is still 1'); is($a->dump_debug_messages(), 0, 'dump_debug_messages on a is still 0'); is($b->dump_debug_messages(), 1, 'dump_debug_messages on b is still 1'); # Make sure classes inherit their messaging behavior from parents if they don't # otherwise change it ok(UR::Object::Type->define( class_name => 'Parent'), 'Define class Parent'); ok(UR::Object::Type->define( class_name => 'ChildA', is => 'Parent'), 'Define class ChildA'); ok(UR::Object::Type->define( class_name => 'ChildB', is => 'Parent'), 'Define class ChildB'); $a = ChildA->create(); ok($a, 'Create object a'); $b = ChildB->create(); ok($b, 'Create object b'); is(Parent->dump_debug_messages(), undef, 'Parent dump_debug_messages() starts off as undef'); is(Parent->dump_debug_messages(0), 0, 'Setting Parent dump_debug_messages() to 0'); is(ChildA->dump_debug_messages(), 0, 'ChildA dump_debug_messages() is 0'); is($a->dump_debug_messages(), 0, 'object a dump_debug_messages() is 0'); is(ChildB->dump_debug_messages(), 0, 'ChildB dump_debug_messages() is 0'); is($b->dump_debug_messages(), 0, 'object b dump_debug_messages() is 0'); # Change the parent and the child classes and instances inherit it, since they haven't # overriden anything yet foreach my $val ( 1, 0 ) { is(Parent->dump_debug_messages($val), $val, "Change Parent dump_debug_messages() to $val"); is(Parent->dump_debug_messages(), $val, 'Parent dump_debug_messages() is set'); is(ChildA->dump_debug_messages(), $val, 'ChildA dump_debug_messages() matches Parent'); is($a->dump_debug_messages(), $val, 'object a dump_debug_messages() matches Parent'); is(ChildB->dump_debug_messages(), $val, 'ChildB dump_debug_messages() matches Parent'); is($b->dump_debug_messages(), $val, 'object b dump_debug_messages() matches Parent'); } # Twiddle both the parent and one of the child classes foreach my $parent_val ( 2, 1, 0) { is(Parent->dump_debug_messages($parent_val), $parent_val, "Set Parent dump_debug_messages() to $parent_val"); foreach my $child_val ( 1, 0 ) { is(ChildA->dump_debug_messages($child_val), $child_val, "Change ChildA dump_debug_messages() to $child_val"); is(ChildA->dump_debug_messages(), $child_val, 'ChildA dump_debug_messages() is set'); is($a->dump_debug_messages(), $child_val, 'object a dump_debug_messages() matches ChildA'); is(Parent->dump_debug_messages(), $parent_val, 'Parent dump_debug_messages() is still set'); is(ChildB->dump_debug_messages(), $parent_val, 'ChildB dump_debug_messages() matches Parent'); is($b->dump_debug_messages(), $parent_val, 'object b dump_debug_messages() matches Parent'); } } my $a2 = ChildA->create(); my $b2 = ChildB->create(); # Explicity set all the invilved entities is(Parent->dump_debug_messages(1), 1, ' Set Parent dump_debug_messages() to 1'); is(ChildA->dump_debug_messages(2), 2, ' Set ChildA dump_debug_messages() to 2'); is(ChildB->dump_debug_messages(3), 3, ' Set Parent dump_debug_messages() to 3'); is($a->dump_debug_messages(4), 4, ' Set object a dump_debug_messages() to 4'); is($a2->dump_debug_messages(5), 5, ' Set object a2 dump_debug_messages() to 5'); is($b->dump_debug_messages(6), 6, ' Set object b dump_debug_messages() to 6'); is($b2->dump_debug_messages(7), 7, ' Set object b dump_debug_messages() to 7'); # Check the values is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 2, 'ChildA dump_debug_messages() is 2'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 4, 'object a dump_debug_messages() is 4'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); # Now, start setting some of them to undef, meaning they should again inherit from their parent #diag('Clear setting on object a'); $a->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 2, 'ChildA dump_debug_messages() is 2'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 2, 'object a dump_debug_messages() is now 2'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on ChildA'); ChildA->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 5, 'object a2 dump_debug_messages() is 5'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object a2'); $a2->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 6, 'object b dump_debug_messages() is 6'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object b'); $b->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 3, 'Parent dump_debug_messages() is 3'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 3, 'object b dump_debug_messages() is 3'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on ChildB'); ChildB->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 7, 'object b dump_debug_messages() is 7'); #diag('Clear setting on object b2'); $b2->dump_debug_messages(undef); is(Parent->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 1, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 1, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 1, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 1, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 1, 'object b dump_debug_messages() is 1'); my @ENV_VARS = ('UR_DUMP_DEBUG_MESSAGES', 'UR_COMMAND_DUMP_DEBUG_MESSAGES'); $DB::single=1; foreach $var ( @ENV_VARS ) { delete $ENV{$_} foreach @ENV_VARS; # clear them first #diag("use the $var env var"); $ENV{$var} = 99; is(Parent->dump_debug_messages(), 99, 'Parent dump_debug_messages() is 1'); is(ChildA->dump_debug_messages(), 99, 'ChildA dump_debug_messages() is 1'); is(ChildB->dump_debug_messages(), 99, 'Parent dump_debug_messages() is 1'); is($a->dump_debug_messages(), 99, 'object a dump_debug_messages() is now 1'); is($a2->dump_debug_messages(), 99, 'object a2 dump_debug_messages() is 1'); is($b->dump_debug_messages(), 99, 'object b dump_debug_messages() is 1'); is($b2->dump_debug_messages(), 99, 'object b dump_debug_messages() is 1'); } 87_is_many_indirect_is_efficient.t000444023532023421 1273212121654174 21560 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 13; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Tests that calling car_parts_prices on a URT::Person object is efficient # The AccessorWriter does the retrieval differently if these conditions hold: # 1) car_parts_prices is_delegated and is_many # 2) the thing it is 'via' (cars) is an object accessor (has a data_type: URT::Car) and is_many # 3) the thing it is 'to' (parts_prices) is_delegated and has a via as well # 4) parts_prices is via something that is an object accessor with a data_type (URT::CarParts) # # If these hold, then it can do the query differently: # 1) Call URT::Car->get() with appropriate params that $person->cars would use # 2) For each of the objects resulting from #1 (URT::Car), extract out the value that links # these to the final class (URT::CarParts) # 3) Do a get on the final class filtering on the linking property and an 'in' clause # with the values from #2 use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color' }, car_parts => { is => 'URT::CarParts', via => 'cars', to => 'parts', is_optional => 1, is_many => 1 }, car_parts_prices => { via => 'cars', to => 'parts_prices', is_optional => 1, is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, parts_prices => { via => 'parts', to => 'price', is_many => 1}, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @colors = $person->cars(); is(scalar(@colors), 2, 'person has 2 cars with colors'); is($query_count, 1, 'made 1 query'); $query_count = 0; my @prices = $person->car_parts_prices(); is(scalar(@prices), 4, "person's cars have 4 car_parts with prices"); is($query_count, 1, 'Made 1 query'); 86_custom_load.t000444023532023421 300012121654174 16005 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use UR; package Foo; class Foo { id_by => ['a'], #has => ['b','c', 'd' => { calculate_from => ['b','c'], calculate => q|$b+$c| }] has => [qw/a b c/], }; sub __load__ { return ['id', 'a', 'b', 'c'], [ ['a1', 'a1', 'b1', 'c1'], ['a2', 'a2', 'b2', 'c2'], ['a3', 'a3', 'b3', 'c3'], ] }; package main; use Test::More tests=> 16; my $o1 = Foo->get('a2'); ok($o1, "got object 2 back"); is($o1->id, 'a2', 'id is correct'); is($o1->a, 'a2', 'property a is correct'); is($o1->b, 'b2', 'property b is correct'); is($o1->c, 'c2', 'property c is correct'); my @o = Foo->get(); is(scalar(@o), 3, "got objects back"); package Bar; class Bar { id_by => 'a', has => [qw/a b c/] }; my $data_set_size = 100_000; sub __load__ { my $props = ['id','a','b','c']; my $data = IO::File->new("yes abcdefg| head -n $data_set_size |"); my $n = 0; my $iterator = sub { my $v = $data->getline; if (not defined $v) { $data->close(); return; } chomp $v; $n++; return [$n,$n,$v,$v]; }; return ($props, $iterator); } package main; my $i = Bar->create_iterator(); my $n = 0; while (my $o = $i->next()) { $n++; if ($n % 10_000 == 0) { ok(1,"processed $n"); } } 37_caching_with_in_clause.t000444023532023421 1217012121654174 20171 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 61; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); my $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(name => [1,2,3,4,5]); is(scalar(@o), 5, 'get() returned the correct number of items with an in clause'); is($load_count, 5, 'loaded 5 objects'); is($query_count, 1, '1 query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [1,2,3,4,5]); is(scalar(@o), 5, 'get() returned the correct number of items with the same in clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [2,3,4]); is(scalar(@o), 3, 'get() returned the correct number of items with a subset in clause'); is($load_count, 0, 'loaded 0 new objects'); #is($query_count, 0, 'no query was generated (known broken)'); foreach my $id ( 1 .. 5 ) { $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => $id); is(scalar(@o), 1, 'get() returned 1 item with a single id'); is($load_count, 0, 'no new objects were loaded'); is($query_count, 0, 'no new queries were done'); } $load_count = 0; $query_count = 0; # Note that it's probably not worth it for the query system to remove 4 and 5 # before it constructs the SQL query @o = URT::Parent->get(name => [4,5,6,7]); is(scalar(@o), 4, 'get() returned the correct number of items with another in clause'); is($load_count, 2, '2 new objects were loaded'); is($query_count, 1, '1 new query was done'); # FIXME - subscriptions for 'query' doesn't pass along the SQL to the callback #ok($query_text !~ m/4,5,6,7/, q(Generated query does not mention "('4','5','6','7')")); #ok($query_text =~ m/6,7/, q(Generated query does mention "('6','7')")); $load_count = 0; $query_count = 0; my $iter = URT::Parent->create_iterator(name => [5,7,2,99,102], is_cool => 1); ok($iter, 'Created iterator with an in-clause'); ok($iter->next, 'Pull an object off the iterator'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $iter = undef; $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => [5,7,2,99,102], is_cool => 1); is(scalar(@o), 3, 'get() returned the correct number of items with in clause containing some non-matching values'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 102, is_cool => 1,); is(scalar(@o), 0, 'get() correctly returns nothing for a non-matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 99, is_cool => 1,); is(scalar(@o), 0, 'get() correctly returns nothing for another non-matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 5); is(scalar(@o), 1, 'got one object by name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); $load_count = 0; $query_count = 0; @o = URT::Parent->get(name => 99); is(scalar(@o), 1, 'There was one with name 99'); is($load_count, 1, 'loaded 0 new objects'); is($query_count, 1, 'no query was generated'); unlink(URT::DataSource::SomeSQLite->server); # Remove the DB file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, is_cool => { is => 'NUMBER' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Parent"); my $sth = $dbh->prepare('insert into parent_table (parent_id, name, is_cool) values (?,?,?)'); ok($sth,'insert statement prepared'); foreach my $n ( 1 .. 10 ) { ok($sth->execute($n,$n,1), "inserted parent ID $n"); } $sth->execute(99,99,0); # item 99 is not cool } 49c_complicated_get_3.t000444023532023421 1051312121654174 17232 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 11; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The property we're filtering on is doubly delegated # - Each class through the indirection has a parent class with a table &setup_classes_and_db(); my $person = URT::Person->get(animal_breed_is_smart => 1); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Jeff', 'The expected object was returned'); is($person->animal_name, 'Lassie', 'the delegated property has the expected value'); is($person->animal_breed_name, 'Collie', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # Schema/class design # NamedThing is things with names... parent class for the other classes # Person is-a NamedThing, it has an Animal with animal_name, and the animal has a animal_breed_name # Animal is-a NamedThing. it has a AnimalBreed with a breed_name # AnimalBreed is-a NamedThing. It has a name ok( $dbh->do("create table named_thing (named_thing_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created named_thing table'); ok( $dbh->do("create table breed (breed_id PRIMARY KEY REFERENCES named_thing(named_thing_id), is_smart integer NOT NULL)"), 'created animal breed table'); ok( $dbh->do("create table animal (animal_id PRIMARY KEY REFERENCES named_thing(named_thing_id), breed_id REFERENCES breed(breed_id))"), 'created animal table'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY REFERENCES named_thing(named_thing_id), animal_id integer REFERENCES animal(animal_id))"), 'Created people table'); my $name_insert = $dbh->prepare('insert into named_thing (named_thing_id, name) values (?,?)'); my $breed_insert = $dbh->prepare('insert into breed (breed_id, is_smart) values (?,?)'); my $animal_insert = $dbh->prepare('insert into animal (animal_id, breed_id) values (?,?)'); my $person_insert = $dbh->prepare('insert into person (person_id,animal_id) values (?,?)'); # Insert a breed named Collie $name_insert->execute(1, 'Collie'); $breed_insert->execute(1,1); # A Dog named Lassie $name_insert->execute(2, 'Lassie'); $animal_insert->execute(2, 1); # a person named Jeff $name_insert->execute(3, 'Jeff'); $person_insert->execute(3,2); $name_insert->finish; $breed_insert->finish; $animal_insert->finish; $person_insert->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::NamedThing', id_by => [ named_thing_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], is_abstract => 1, data_source => 'URT::DataSource::SomeSQLite', table_name => 'named_thing', ); UR::Object::Type->define( class_name => 'URT::Breed', is => 'URT::NamedThing', id_by => ['breed_id'], has => [ is_smart => { is => 'Boolean', }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'breed', ); UR::Object::Type->define( class_name => 'URT::Animal', is => 'URT::NamedThing', id_by => ['animal_id'], has => [ breed => { is => 'URT::Breed', id_by => 'breed_id' }, breed_name => { via => 'breed', to => 'name' }, breed_is_smart => { via => 'breed', to => 'is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'animal', ); UR::Object::Type->define( class_name => 'URT::Person', is => 'URT::NamedThing', id_by => ['person_id'], has => [ animal => { is => 'URT::Animal', id_by => 'animal_id' }, animal_name => { via => 'animal', to => 'name' }, animal_breed_name => { via => 'animal', to => 'breed_name' }, animal_breed_is_smart => { via => 'animal', to => 'breed_is_smart' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); } 82a_boolexpr_op_case_insensitive.t000444023532023421 200412121654174 21577 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; use UR; class Foo { has => [ _bar => {}, _baz => {}, ], }; sub evaluate_permutations_for_boolexps_with_message { my $bx1 = shift; my $bx2 = shift; my $msg = shift; for my $obj (Foo->create( _bar => 0, _baz => 0 ), Foo->create( _bar => 1, _baz => 0 ), Foo->create( _bar => 1, _baz => 1 ), Foo->create( _bar => 0, _baz => 1 )){ is( $bx1->evaluate($obj), $bx2->evaluate($obj), $msg ); } } my $bx1 = Foo->define_boolexpr('_bar != 1 and _baz != 1'); my $bx2 = Foo->define_boolexpr('_bar != 1 AND _baz != 1'); my $bx3 = Foo->define_boolexpr('_bar != 1 or _baz != 1'); my $bx4 = Foo->define_boolexpr('_bar != 1 OR _baz != 1'); evaluate_permutations_for_boolexps_with_message($bx1, $bx2, "Lower and uppercase AND behave the same"); evaluate_permutations_for_boolexps_with_message($bx3, $bx4, "Lower and uppercase OR behave the same"); done_testing(); 91_object_sets.t000444023532023421 2605712121654174 16035 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, car_count => { via => 'car_set', to => 'count' }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, primary_car_uc_color => { via => 'primary_car', to => 'uc_color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, uc_color => { calculate_from => ['color'], calculate => q( return uc($color) ) }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my @c = URT::Car->get(owner_id => 13, "is_primary true" => 1); $query_count = 0; my $set = URT::Person->define_set('age <' => 20); ok($set, 'Defined set of people younger than 20'); is($query_count, 0, 'Made no queries'); $query_count = 0; my $count = $set->count(); is($count, 0, 'Set count is 0'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @members = $set->members(); is(scalar(@members), 0, 'Set has no members'); is($query_count, 1, 'Made 1 query'); # the above query for count didn't actually retrieve the members $query_count = 0; $set = URT::Person->define_set(is_cool => 1); ok($set, 'Defined set of cool people'); is($query_count, 0, 'Made no queries'); $query_count = 0; $count = $set->count(); is($count, 3, '3 people are cool'); is($query_count, 1, 'Made 1 query'); $query_count = 0; @members = $set->members(); is_deeply([ map { $_->name } @members], ['Bob','Joe','Frank'], 'Got the right members'); is($query_count, 1, 'Made one query'); # again, getting the count didn't load the members $query_count = 0; $set = URT::Person->define_set(); ok($set, 'Defined set of all people'); is($query_count, 0, 'Made no queries'); $query_count = 0; my @subsets = $set->group_by('car_colors'); is(scalar(@subsets), 4, 'Partitioning all people by car_colors yields 4 subsets'); is($query_count, 4, 'Made 4 queries'); # 3 to index the car_color for the 3 owners already loaded, 1 more for the group_by car_color # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my %people_by_car_color = ( 'red' => ['Bob', 'Mike'], 'blue' => ['Fred', 'Joe'], 'yellow' => ['Bob'], '' => ['Frank'], ); foreach my $subset ( @subsets ) { my $query_count = 0; my @colors = $subset->car_colors; is($#colors, 0, "one color returned") or diag "@colors"; my $color = shift @colors; is($query_count, 0, 'Getting car_colors from subset made no queries'); $query_count = 0; @members = $subset->members(); is($query_count, 0, 'Getting members from subset made no queries'); my $expected_members = $people_by_car_color{$color || ''}; is(scalar(@members), scalar(@$expected_members), 'Got the expected number of subset members'); is_deeply([ map { $_->name } @members], $expected_members, 'Their names were correct'); } $query_count = 0; $set = URT::Person->define_set(is_cool => 0); ok($set, 'Defined set of poeple that are not cool'); is($query_count, 0, 'Made no queries'); my %color_subsets; my %colors = ( 'pink' => [], 'red' => ['Mike'], 'blue' => ['Fred','Joe'] ); foreach my $color (keys %colors) { $query_count = 0; my $subset = $set->subset(primary_car_color => $color); ok($subset, "Defined a subset where primary_car_color is $color"); is($query_count, 0, 'Made no queries'); $color_subsets{$color} = $subset; } my $first_time = 1; foreach my $color ( keys %colors ) { my $subset = $color_subsets{$color}; $query_count = 0; my @names = $subset->name; my $expected_names = $colors{$color}; is(scalar(@names), scalar(@$expected_names), "Calling 'name' on the $color subset has the right number of names"); is_deeply(\@names, $expected_names, 'The names are correct'); is($query_count, 1, 'query count is correct'); $first_time = 0; } # Make a set that includes a filtered calculated property $query_count = 0; $set = URT::Car->define_set(uc_color => 'nomatches'); ok($set, 'Defined set of cars filtered by uc color that will not match anything'); is($query_count, 0, 'Made no queries'); is($set->count, 0, 'That set is empty'); ok($query_count, 'Made a query'); $query_count = 0; $set = URT::Person->define_set(primary_car_uc_color => 'wontmatch'); ok($set, 'Defined set of people filtered by uc color that will not match anything'); is($query_count, 0, 'Made no queries'); is($set->count, 0, 'That set is empty'); ok($query_count, 'Made a query'); # Test having an -order_by in addition to -group_by. It should throw an exception if # all the order_by columns don't appear in -group_by. # To mix it up a bit, we'll unload the peole with yellow cars. # This will require that it not do the sets entirely from cached objects. for my $person (URT::Person->is_loaded(car_colors => 'yellow')) { $person->unload; } # Now we'll delete the people with blue cars. This means we should not get a set # back even though the set is in the database. This will require that changes # check for intersecting sets and remove cached aggregate values. Because # we already track loaded object queries in a 2-tier hash (all_params_loaded), # we probably need a symmetrical structure for loaded sets to make this efficient. # This will ensure that, while creation and deletion must test all sets for membership, # updates will only need to look at sets with templates which involve the changed properties. #for my $person (URT::Person->is_loaded(car_colors => 'blue')) { # $person->delete; #} $query_count = 0; @subsets = URT::Person->get(-group_by => ['car_colors'], -order_by => ['car_colors']); is(scalar(@subsets), 4, 'Partitioning all people by car_colors yields 4 subsets, this time with order_by'); foreach (@subsets) { isa_ok($_, 'URT::Person::Set'); } is_deeply([ map { $_->car_colors } @subsets ], [undef, 'blue', 'red', 'yellow'], 'The color subsets were returned in the correct order'); is($query_count, 1, 'query count is correct'); @subsets = eval { URT::Person->get(-group_by => ['is_cool'], -order_by => ['car_colors'])}; is(scalar(@subsets), 0, 'Partitioning all people by is_cool, order_by car_colors returned no subsets'); like($@, qr(^Property 'car_colors' in the -order_by list must appear in the -group_by list), 'It threw the correct exception'); my $bob = URT::Person->get(name => 'Bob'); is($bob->car_count, 2, 'Bob has 2 cars using the set'); my $fred = URT::Person->get(name =>'Fred'); is($fred->car_count, 1, 'Fred has 1 car using the set'); my $frank = URT::Person->get(name => 'Frank'); is($frank->car_count, 0, 'Frank has 0 cars using the set'); do { # Class methods that are not implemented on the Set should be delegated # to the member class and should not be handled by the (immutable) # instance accesors. my $_some_member_method = ''; local *URT::Person::_some_member_method = sub { $_some_member_method = [@_] }; my $_some_member_method_can = URT::Person::Set->can('_some_member_method'); $@ = ''; eval { $_some_member_method_can->('URT::Person::Set', 42) }; my $error = $@; is($error, '', 'no error when calling _some_member_method on set class'); is_deeply( $_some_member_method, ['URT::Person', 42], '_some_member_method was delegated to member class' ); }; do { # Class methods that are implemented on the Set should be called as # normal and should not be handled by the (immutable) instance accesors. my $_some_set_method = 0; local *URT::Person::Set::_some_set_method = sub { $_some_set_method = [@_] }; my $_some_set_method_can = URT::Person::Set->can('_some_set_method'); $@ = ''; eval { $_some_set_method_can->('URT::Person::Set', 42) }; my $error = $@; is($error, '', 'no error when calling _some_set_method on set class'); is_deeply( $_some_set_method, ['URT::Person::Set', 42], '_some_set_method was not delegated to member class' ); }; do { # Instance methods should still be handled by the (immutable) member class # accessors. my $_some_member_method = ''; local *URT::Person::_some_member_method = sub { $_some_member_method = [@_] }; my $set = URT::Person->define_set(); my $_some_member_method_can = $set->can('_some_member_method'); $@ = ''; eval { $_some_member_method_can->($set, 42) }; my $error = $@; like($error, qr/_some_member_method/, 'got error when calling _some_member_method as a mutator on a set object'); }; done_testing(); 21b_load_observer_autosubclass.t000444023532023421 1075112121654174 21274 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 27; # Make an abstract class with a table, and a child class with a table # The 'load' signal should only ever be fired once for each object loaded my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, subclass_name varchar)'), 'create person table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 'URT::Employee')"), 'insert into person table'); ok($dbh->do('CREATE TABLE employee (employee_id integer, office varchar)'), 'create employee table'); ok($dbh->do("INSERT into employee VALUES (1, '1')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (2, '2')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (3, '3')"), 'insert into employee table'); ok($dbh->do("INSERT into employee VALUES (4, '4')"), 'insert into employee table'); UR::Object::Type->define( class_name => 'URT::Person', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'person_id', has => [ name => { is => 'String' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Employee', is => 'URT::Person', id_by => 'employee_id', has => [ office => { is => 'String'} , ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'employee', ); my @person_observations; my $person_obv = URT::Person->add_observer(callback => sub { my($obj,$method) = @_; push @person_observations, [$method, $obj->class, $obj->id]; }); ok($person_obv, "made an observer on Person class"); my @employee_observations; my $employee_obv = URT::Employee->add_observer(callback => sub { my($obj,$method) = @_; push @employee_observations, [$method, $obj->class, $obj->id]; }); ok($employee_obv, "made an observer on Employee class"); @person_observations = (); @employee_observations = (); my $person = URT::Person->get(1); ok($person, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 1] ], # subclasses/loaded as Employee 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 1] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); $person = URT::Employee->get(2); ok($person, 'Got Employee ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 2] ], 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ [ 'load', 'URT::Employee', 2] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); my @people = URT::Person->get(); is(scalar(@people), 4, 'Got 4 Person objects'); is(scalar(@person_observations), 2, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Person observations match expected'); is(scalar(@employee_observations), 2, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Employee observations match expected'); 03i_rule_hard_refs.t000444023532023421 453712121654174 16636 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl # Test handling of rules and their values with different kinds # params. use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 9; use Data::Dumper; use IO::Handle; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, foo => { is => "String", is_optional => 1 }, fh => { is => "IO::Handle", is_optional => 1 }, scores => { is => 'ARRAY' }, things => { is => 'HASH' }, relateds => { is => 'URT::Related', reverse_as => 'item', is_many => 1 }, related_ids => { via => 'relateds', to => 'id', is_many => 1 }, ] }; class URT::Related { has => { item => { is => 'URT::Item', id_by => 'item_id' }, } }; my $scores = [1,2,3]; my $things = {'one' => 1, 'two' => 2, 'three' => 3}; my $related_ids = [1,2,3]; my $rule = URT::Item->define_boolexpr(name => 'Bob', scores => $scores, things => $things, related_ids => $related_ids); ok($rule, 'Created boolexpr'); is($rule->value_for('name'), 'Bob', 'Value for name is correct'); is($rule->value_for('scores'), $scores, 'Getting the value for "scores" returns the exact same array as was put in'); is($rule->value_for('things'), $things, 'Getting the value for "things" returns the exact same hash as was put in'); is($rule->value_for('related_ids'), $related_ids, 'Getting the value for "related_ids" does not return the exact same array as was put in'); my $tmpl = UR::BoolExpr::Template->resolve('URT::Item', 'name','scores','things','related_ids'); ok($tmpl, 'Created BoolExpr template'); my $rule_from_tmpl = $tmpl->get_rule_for_values('Bob', $scores, $things,$related_ids); #ok($rule_from_tmpl, 'Created BoolExpr from that template'); TODO: { local $TODO = "rules created from get_rule_for_values() don't have their hard refs properly saved"; is($rule_from_tmpl->value_for('scores'), $scores, 'Getting the value for "scores" returns the exact same array as was put in'); is($rule_from_tmpl->value_for('things'), $things, 'Getting the value for "things" returns the exact same hash as was put in'); is($rule_from_tmpl->value_for('related_ids'), $related_ids, 'Getting the value for "related_ids" does not return the exact same array as was put in'); } 90_comparison_value_and_escape_character_to_regex.t000444023532023421 260312121654174 25117 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More; use UR::BoolExpr::Template::PropertyComparison::Like; package Foo; class Foo { id_by => ['a'], has => [qw/a b c/], }; package main; for my $sc ('(', ')', '{', '}', '[', ']', '?', '.', '+', '|', '-') { my $value = "foo$sc"; my $esc = quotemeta($sc); my $expected_escaped_value = qr|^foo$esc$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly escaped $sc"); } { my $value = "foo%"; my $expected_escaped_value = qr|^foo.*$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly changed '%' to wildcard"); } { my $value = "foo_"; my $expected_escaped_value = qr|^foo.$|; my $escaped_value = UR::BoolExpr::Template::PropertyComparison::Like->comparison_value_and_escape_character_to_regex($value); is($escaped_value, $expected_escaped_value, "properly changed '_' to wildcard"); } my $create_object = Foo->create(a => '0', b => 'foo)bar'); is(ref $create_object, 'Foo', 'created a Foo'); my $get_object = Foo->get('b like' => 'foo)%'); is(ref $get_object, 'Foo', 'got object that was just created using like with special char'); done_testing(); 80c_command_describe.t000444023532023421 277312121654174 17127 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 3; my $cmd = UR::Namespace::Command::Show::Properties->create(classes_or_modules => ['URT::Thingy'], namespace_name => 'URT'); ok($cmd, 'Create UR::Namespace::Command::Show::Properties'); my $output = ''; close STDOUT; open(STDOUT, '>', \$output) || die "Can't open STDOUT: $!"; ok($cmd->execute(), 'Execute()'); my $expected_output = < 41; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); our $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::Employee', has => [ first_name => { type => "String" }, last_name => { type => "String" }, full_name => { calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', }, user_name => { calculate_from => ['first_name','last_name'], calculate => 'lc(substr($first_name,0,1) . substr($last_name,0,5))', }, email_address => { calculate_from => ['user_name'] }, cached_uc_full_name => { is_constant => 1, calculate => q( $main::calculate_called = 1; return uc($self->full_name); ), }, ] ); sub Acme::Employee::email_address { my $self = shift; return $self->user_name . '@somewhere.tv'; } $calculate_called = 0; my $e1 = Acme::Employee->create(first_name => "John", last_name => "Doe"); ok($e1, "created an employee object"); ok($e1->can("full_name"), "employees have a full name"); ok($e1->can("user_name"), "employees have a user_name"); ok($e1->can("email_address"), "employees have an email_address"); is($e1->full_name,"John Doe", "name check works"); is($e1->user_name, "jdoe", "user_name check works"); is($e1->email_address, 'jdoe@somewhere.tv', "email_address check works"); is($calculate_called, 0, 'The cached calculation sub has not been called yet'); $calculate_called = 0; my $saved_uc_full_name = uc($e1->full_name); is($e1->cached_uc_full_name, $saved_uc_full_name, 'calculated + cached upper-cased name is correct'); is($calculate_called, 1, 'The calculation function was called'); $e1->first_name("Jane"); $e1->last_name("Smitharoonie"); is($e1->full_name,"Jane Smitharoonie", "name check works after changes"); is($e1->user_name, "jsmith", "user_name check works after changes"); is($e1->email_address, 'jsmith@somewhere.tv', "email_address check works"); $calculate_called = 0; is($e1->cached_uc_full_name, $saved_uc_full_name, 'calculated + cached upper-cased name is correct'); is($calculate_called, 0, 'The calculation function was not called'); isnt($e1->cached_uc_full_name, uc($e1->full_name), 'it is correctly different than the current upper-case full name'); UR::Object::Type->define( class_name => "Acme::LineItem", has => [ quantity => { type => 'Number' }, unit_price => { type => 'Money' }, sum_total => { type => 'Money', calculate => 'sum', calculate_from => ['quantity','unit_price'] }, sub_total => { type => 'Money', calculate => 'product', calculate_from => ['quantity','unit_price'] }, ], ); my $line = Acme::LineItem->create(quantity => 5, unit_price => 2); ok($line, "made an order line item"); is($line->sum_total,7, "got the correct sum-total"); is($line->sub_total,10, "got the correct sub-total"); # Make a cached+calculated property that is also saved in the database use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table thing (thing_id integer, name varchar, munged_name varchar)'); $dbh->do("insert into thing values (1234,'Bob', 'munged Bob')"); $dbh->do("Insert into thing values (2345,'Fred', null)"); $calculate_called = 0; UR::Object::Type->define( class_name => 'Acme::SavedThing', id_by => 'thing_id', has => [ name => { is => 'String' }, munged_name => { is_mutable => 0, column_name => 'munged_name', calculate_from => ['name'], calculate => sub { my($name) = @_; $calculate_called = 1; return uc($name) }, }, name2 => { calculate_from => ['__self__'], calculate => sub { return $_[0]->name }, }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); $calculate_called = 0; my $new_thing = Acme::SavedThing->create(name => 'Foo'); ok($new_thing, 'Created a SavedThing'); ok($calculate_called, 'Its calculation sub was called'); $calculate_called = 0; is($new_thing->munged_name, 'FOO', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was not called again'); ok(! eval { $new_thing->munged_name('Something else') }, 'Changing munged_name correctly returned false'); ok($@, 'Trying to change munged_name generated an exception'); $calculate_called = 0; $new_thing = Acme::SavedThing->create(name => 'Bar', munged_name => 'Something else'); ok($new_thing, 'Created another SavedThing'); is($calculate_called, 0, 'The calculation sub was not called'); is($new_thing->munged_name, 'Something else', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was still not called'); $calculate_called = 0; $new_thing = Acme::SavedThing->get(name => 'Bob'); ok($new_thing, 'Got a SavedThing from the DB'); is($new_thing->munged_name, 'munged Bob', 'The munged_name property is correct'); is($calculate_called, 0, 'The calculation sub was not called'); $calculate_called = 0; $new_thing = Acme::SavedThing->get(name => 'Fred'); ok($new_thing, 'Got another SavedThing from the DB'); is($new_thing->munged_name, undef, 'The munged_name property is correctly undef'); is($calculate_called, 0, 'The calculation sub was not called'); is($new_thing->name, $new_thing->name2, 'calling calculated sub where calculate_from includes __self__ works'); ok(UR::Context->commit, 'Saved to the DB'); my @row = $dbh->selectrow_array(q(select thing_id, name, munged_name from thing where name = 'Foo')); ok(scalar(@row), 'Retrieved row from DB where name is Foo'); is($row[2], 'FOO', 'Saved munged_name is correct'); @row = $dbh->selectrow_array(q(select thing_id, name, munged_name from thing where name = 'Bar')); ok(scalar(@row), 'Retrieved row from DB where name is Bar'); is($row[2], 'Something else', 'Saved munged_name is correct'); 13c_message_observers.t000444023532023421 270612121654174 17357 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use strict; use warnings; use Test::More; plan tests => 8; ok(UR::Object::Type->define( class_name => 'Parent', is_abstract => 1), 'Define Parent class'); ok(UR::Object::Type->define( class_name => 'ChildA', is => 'Parent'), 'Define class ChildA'); ok(UR::Object::Type->define( class_name => 'ChildB', is => 'Parent'), 'Define class ChildB'); my $a = ChildA->create(id => 1); ok($a, 'Create object a'); my $b = ChildB->create(id => 1); ok($b, 'Create object b'); is(Parent->dump_status_messages(0), 0, 'Turn off dump_status_messages'); my %callbacks_fired; foreach my $class (qw( Parent ChildA ChildB )) { $class->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{$class}++; }); } $a->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{'objecta'}++; }); $b->add_observer(aspect => 'status_message', callback => sub { $callbacks_fired{'objectb'}++; }); ok($a->status_message('Hi'), 'sent status message to object a'); is_deeply(\%callbacks_fired, { Parent => 1, ChildA => 1, objecta => 1 }, 'Callbacks fired correctly'); 61a_iterator_with_or_boolexpr.t000444023532023421 456412121654174 21144 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 16; use URT::DataSource::SomeSQLite; # This tests a get() with some "or"s in the boolexpr. &setup_classes_and_db(); my $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(id<0) or (name=Diane)', ); ok($boolexpr, 'defined boolexpr'); my $iter = URT::Thing->create_iterator($boolexpr); my $count = 0; my $thing = undef; while(my $next = $iter->next) { $count++; $thing = $next; } is($count, 1, 'found one thing'); is($thing->id, 3, 'is correct object'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Bob) or (id<0)', ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; $thing = undef; while(my $next = $iter->next) { $count++; $thing = $next; } is($count, 1, 'found one thing'); is($thing->id, 1, 'is correct object'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Bob) or (type_id=8)' ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; while($iter->next) { $count++; } is($count, 2, 'found two things'); $boolexpr = UR::BoolExpr->resolve_for_string( 'URT::Thing', '(name=Christine) or (id>0)' ); ok($boolexpr, 'defined boolexpr'); $iter = URT::Thing->create_iterator($boolexpr); $count = 0; while($iter->next) { $count++; } is($count, 3, 'found all three things (with no duplicates)'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, name varchar, type_id integer)"), 'Created thing table'); my $ins_thing = $dbh->prepare("insert into thing (thing_id, name, type_id) values (?,?,?)"); foreach my $row ( ( [1, 'Bob',1], [2, 'Christine',2], [3, 'Diane', 8]) ) { ok( $ins_thing->execute(@$row), 'Inserted a thing'); } $ins_thing->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, type_id => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 96b_ur_context_class_commit_triggers_observer.t000444023532023421 133412121654174 24413 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; __PACKAGE__->main('UR'); sub main { my ($test, $module) = @_; use_ok($module) or exit; $test->ur_context_class_commit_triggers_observer; done_testing(); } sub ur_context_class_commit_triggers_observer { my $self = shift; my $context = UR::Context->current; ok(UR::Context->commit, 'UR::Context committed'); my $commit_callback_ran; my $commit_callback = sub { $commit_callback_ran = 1; }; $context->add_observer( aspect => 'commit', callback => $commit_callback, ); ok(UR::Context->commit, 'UR::Context committed'); is($commit_callback_ran, 1, 'commit_callback ran'); } 03g_rule_constant_key_before.t000444023532023421 474412121654174 20722 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use UR; use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; ok(setup_classes_and_db(), 'setup classes and DB') or die; # 'constant key before' tests were added due to bug that occurred when constant keys were specified # before expanded properties. Since values are split based on whether they are constant (go on template) # or non-constant (go on rule) there was a mismatch when a rule was normalized. do { # This would work BUT was not equivalent to switching the -order and id k/v pairs. my @phone = UR::Context->current->reload('Phone', id => [0], -order => []); is(scalar @phone, 1, 'constant key after expanded property (op: in)'); }; do { # This would work since make is not expanded. my @phone = UR::Context->current->reload('Phone', -order => [], make => ['Nokia']); is(scalar @phone, 1, 'constant key before non-expanded property'); }; do { my @phone = UR::Context->current->reload('Phone', -order => [], id => [0]); is(scalar @phone, 1, 'constant key before expanded property (op: in)'); }; do { my @phone = UR::Context->current->reload('Phone', -order => [], id => 0); is(scalar @phone, 1, 'constant key before expanded property (op: eq)'); }; done_testing(); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh, 'got DB handle'); ok($dbh->do('create table phones (phone_id integer, make varchar, model varchar)'), 'created phones table'); my @phone_specs = ( ['Motorola', 'Atrix'], ['Motorola', 'Droid Razr'], ['Nokia', 'N9'], ); my $insert = $dbh->prepare('insert into phones (phone_id, make, model) values (?,?,?)'); for (my $id = 0; $id < @phone_specs; $id++) { unless ($insert->execute($id, @{$phone_specs[$id]})) { die "Couldn't insert a row into 'phones': $DBI::errstr"; } } $dbh->commit; my $phone_type = UR::Object::Type->define( class_name => 'Phone', id_by => [ phone_id => { is => 'Number' }, ], has => [ make => { is => 'Text' }, model => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'phones', ); isa_ok($phone_type, 'UR::Object::Type', 'defined Phone class'); is(Phone->class, 'Phone', 'Phone class is loaded'); return 1; } 01_object.t000444023532023421 47312121654174 14720 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $o = URT::Thingy->create(id => 111); ok($o, "made an object"); 1; 21f_observer_priority.t000444023532023421 670312121654174 17434 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 8; UR::Object::Type->define( class_name => 'URT::Person', has => [ first_name => { is => 'String' }, last_name => { is => 'String' }, full_name => { is => 'String', calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', } ], valid_signals => ['something_else'], ); my $p1 = URT::Person->create( id => 1, first_name => "John", last_name => "Doe" ); ok($p1, "Made a person"); my $p2 = URT::Person->create( id => 2, first_name => "Jane", last_name => "Doe" ); ok($p2, "Made another person"); my $observer_counter = 0; $p1->last_name("DoDo"); is($observer_counter, 0, 'No change in the observer counter when no observers are active'); my %observer_records; my $o1_p1 = $p1->add_observer(callback => sub { $observer_records{'o1_p1'} = $observer_counter++ }, aspect => 'last_name', priority => 9); my $o2_p1 = $p1->add_observer(callback => sub { $observer_records{'o2_p1'} = $observer_counter++ }, aspect => 'last_name', priority => 0); my $o3_p2 = $p2->add_observer(callback => sub { $observer_records{'o3_p2'} = $observer_counter++ }, aspect => 'last_name', priority => 8); my $o4_p2 = $p2->add_observer(callback => sub { $observer_records{'o4_p2'} = $observer_counter++ }, aspect => 'last_name', priority => 1); my $o5_c1 = URT::Person->add_observer(callback => sub { $observer_records{'o5_c1'} = $observer_counter++ }, aspect => 'last_name', priority => 7); my $o6_c1 = URT::Person->add_observer(callback => sub { $observer_records{'o6_c1'} = $observer_counter++ }, priority => 2); my $o7_p1 = $p1->add_observer(callback => sub { $observer_records{'o7_p1'} = $observer_counter++ }, priority => 6); my $o8_p1 = $p1->add_observer(callback => sub { $observer_records{'o8_p1'} = $observer_counter++ }, priority => 3); my $o9_p2 = $p2->add_observer(callback => sub { $observer_records{'o9_p2'} = $observer_counter++ }, priority => 5); my $o10_p2 = $p2->add_observer(callback => sub { $observer_records{'o10_p2'} = $observer_counter++ }, priority => 4); $observer_counter = 0; %observer_records = (); is($p1->last_name("Doh!"),"Doh!", "changed person 1"); is_deeply(\%observer_records, { 'o2_p1' => 0, 'o6_c1' => 1, 'o8_p1' => 2, 'o7_p1' => 3, 'o5_c1' => 4, 'o1_p1' => 5 }, 'Observers fired in the correct order'); ok($o1_p1->priority(-1), 'Change observer priority from lowest to highest'); $observer_counter = 0; %observer_records = (); is($p1->last_name("foo!"),"foo!", "changed person 1"); is_deeply(\%observer_records, { 'o1_p1' => 0, 'o2_p1' => 1, 'o6_c1' => 2, 'o8_p1' => 3, 'o7_p1' => 4, 'o5_c1' => 5 }, 'Observers fired in the correct order'); 04a_sqlite.t000444023532023421 2603612121654175 15163 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 80; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); &setup_schema($dbh); &test_foreign_key_handling(); &test_column_details(); sub test_column_details { my $schema = URT::DataSource::SomeSQLite->default_owner; my $sth = URT::DataSource::SomeSQLite->get_column_details_from_data_dictionary('',$schema,'inline','%'); my @results; while (my $row = $sth->fetchrow_hashref) { my $saved_row; foreach my $key ( qw( TABLE_NAME COLUMN_NAME DATA_TYPE COLUMN_SIZE NULLABLE COLUMN_DEF ) ) { $saved_row->{$key} = $row->{$key}; } push @results, $saved_row; } @results = sort { $a->{'COLUMN_NAME'} cmp $b->{'COLUMN_NAME'} } @results; my @expected = ( { TABLE_NAME => 'inline', COLUMN_NAME => 'id', DATA_TYPE => 'integer', COLUMN_SIZE => undef, NULLABLE => 1, COLUMN_DEF => undef, }, { TABLE_NAME => 'inline', COLUMN_NAME => 'name', DATA_TYPE => 'varchar', COLUMN_SIZE => 255, NULLABLE => 1, COLUMN_DEF => 'some name', }, ); is_deeply(\@results, \@expected, 'column details for table inline are correct'); } sub test_foreign_key_handling { my $expected_fk_data = &make_expected_fk_data(); my @table_names = qw( foo inline inline_s named named_s unnamed unnamed_s named_2 named_2_s unnamed_2 unnamed_2_s); foreach my $table ( @table_names ) { my $found = &get_fk_info_from_dd('','',$table); my $found_count = scalar(@$found); my $expected = $expected_fk_data->{'from'}->{$table}; my $expected_count = scalar(@$expected); is($found_count, $expected_count, "Number of FK rows from $table is correct"); is_deeply($found, $expected, 'FK data is correct'); } foreach my $table ( @table_names ) { my $found = &get_fk_info_from_dd('','','','','',$table); my $found_count = scalar(@$found); my $expected = $expected_fk_data->{'to'}->{$table}; $expected = sort_fk_records($expected); my $expected_count = scalar(@$expected); is($found_count, $expected_count, "Number of FK rows to $table is correct"); is_deeply($found, $expected, 'FK data is correct'); } } unlink URT::DataSource::SomeSQLite->server; sub setup_schema { my $dbh = shift; ok( $dbh->do('CREATE TABLE foo (id1 integer, id2 integer, PRIMARY KEY (id1, id2))'), 'create table (foo) with 2 primary keys'); ok($dbh->do("CREATE TABLE inline (id integer PRIMARY KEY REFERENCES foo(id1), name varchar(255) default 'some name')"), 'create table with one inline foreign key to foo'); ok($dbh->do('CREATE TABLE inline_s (id integer PRIMARY KEY REFERENCES foo (id1) , name varchar)'), 'create table with one inline foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE named (id integer PRIMARY KEY, name varchar, CONSTRAINT named_fk FOREIGN KEY (id) REFERENCES foo (id1))'), 'create table with one named table constraint foreign key to foo'); ok($dbh->do('CREATE TABLE named_s (id integer PRIMARY KEY, name varchar, CONSTRAINT named_s_fk FOREIGN KEY(id) REFERENCES foo (id1))'), 'create table with one named table constraint foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE unnamed (id integer PRIMARY KEY, name varchar, FOREIGN KEY (id) REFERENCES foo (id1))'), 'create table with one unnamed table constraint foreign key to foo'); ok($dbh->do('CREATE TABLE unnamed_s (id integer PRIMARY KEY, name varchar, FOREIGN KEY(id) REFERENCES foo(id1))'), 'create table with one unnamed table constraint foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE named_2 (id1 integer, id2 integer, name varchar, PRIMARY KEY (id1, id2), CONSTRAINT named_2_fk FOREIGN KEY (id1, id2) REFERENCES foo (id1,id2))'), 'create table with a dual column named foreign key to foo'); ok($dbh->do('CREATE TABLE named_2_s (id1 integer, id2 integer, name varchar, PRIMARY KEY ( id1 , id2 ) , CONSTRAINT named_2_s_fk FOREIGN KEY( id1 , id2 ) REFERENCES foo( id1 , id2 ) )'), 'create table with a dual column named foreign key to foo, with different whitespace'); ok($dbh->do('CREATE TABLE unnamed_2 (id1 integer, id2 integer, name varchar, PRIMARY KEY (id1, id2), FOREIGN KEY (id1, id2) REFERENCES foo (id1,id2))'), 'create table with a dual column unnamed foreign key to foo'); ok($dbh->do('CREATE TABLE unnamed_2_s (id1 integer, id2 integer, name varchar, PRIMARY KEY( id2 , id2 ) , FOREIGN KEY( id1 , id2 ) REFERENCES foo( id1 , id2 ) )'), 'create table with a dual column unnamed foreign key to foo, with different whitespace'); } sub make_expected_fk_data { my $from = { foo => [], inline => [ { FK_NAME => 'inline_id_foo_id1_fk', FK_TABLE_NAME => 'inline', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], inline_s => [ { FK_NAME => 'inline_s_id_foo_id1_fk', FK_TABLE_NAME => 'inline_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], named => [ { FK_NAME => 'named_fk', FK_TABLE_NAME => 'named', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], named_s => [ { FK_NAME => 'named_s_fk', FK_TABLE_NAME => 'named_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], unnamed => [ { FK_NAME => 'unnamed_id_foo_id1_fk', FK_TABLE_NAME => 'unnamed', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], unnamed_s => [ { FK_NAME => 'unnamed_s_id_foo_id1_fk', FK_TABLE_NAME => 'unnamed_s', FK_COLUMN_NAME => 'id', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, ], named_2 => [ { FK_NAME => 'named_2_fk', FK_TABLE_NAME => 'named_2', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, { FK_NAME => 'named_2_fk', FK_TABLE_NAME => 'named_2', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2' }, ], named_2_s => [ { FK_NAME => 'named_2_s_fk', FK_TABLE_NAME => 'named_2_s', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, { FK_NAME => 'named_2_s_fk', FK_TABLE_NAME => 'named_2_s', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2' }, ], unnamed_2 => [ { FK_NAME => 'unnamed_2_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, { FK_NAME => 'unnamed_2_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2' }, ], unnamed_2_s => [ { FK_NAME => 'unnamed_2_s_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2_s', FK_COLUMN_NAME => 'id1', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id1' }, { FK_NAME => 'unnamed_2_s_id1_id2_foo_id1_id2_fk', FK_TABLE_NAME => 'unnamed_2_s', FK_COLUMN_NAME => 'id2', UK_TABLE_NAME => 'foo', UK_COLUMN_NAME => 'id2' }, ], }; # The 'to' data is just the inverse of 'from' my $to; foreach my $fk_list ( values %$from ) { foreach my $fk ( @$fk_list ) { my $uk_table = $fk->{'UK_TABLE_NAME'}; $to->{$uk_table} ||= []; push @{$to->{$uk_table}}, $fk; my $fk_table = $fk->{'FK_TABLE_NAME'}; $to->{$fk_table} ||= []; } } return { from => $from, to => $to }; } sub get_fk_info_from_dd { my $sth = URT::DataSource::SomeSQLite->get_foreign_key_details_from_data_dictionary(@_); { no warnings 'uninitialized'; ok($sth, "Got a sth to get foreign keys from '$_[2]' to '$_[5]'"); } my @rows; while ( my $row = $sth->fetchrow_hashref() ) { push @rows, $row; } my $rows = sort_fk_records(\@rows); return $rows; } sub sort_fk_records { my($listref) = @_; # no warnings 'uninitialized'; my @sorted = sort { $a->{'FK_TABLE_NAME'} cmp $b->{'FK_TABLE_NAME'} || $a->{'FK_COLUMN_NAME'} cmp $b->{'FK_COLUMN_NAME'} || $a->{'UK_TABLE_NAME'} cmp $b->{'UK_TABLE_NAME'} || $a->{'UK_COLUMN_NAME'} cmp $b->{'UK_COLUMN_NAME'} } @$listref; return \@sorted; } 84_class_definition_errors.t000444023532023421 236112121654175 20415 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', is => 'NonExistentClass', has => 'property_a', ); }; ok(! $o, 'Defining class with non-existant parent did not work'); like($@, qr/cannot initialize because of errors using parent class NonExistentClass/, 'Error message looks correct'); $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', is => 'URT::NonExistentClass', has => 'property_a', ) }; ok(! $o, 'Defining class with non-existant parent did not work'); like($@, qr/cannot initialize because of errors using parent class URT::NonExistentClass/, 'Error message looks correct'); $o = eval { UR::Object::Type->define( class_name => 'URT::Foo', has => [ 'prop' => { is => 'URT::NonExistantClass', id_by => 'prop_id' }, ], ) }; ok(! $o, 'Defining class with relationship to non-existant class did not work'); like($@, qr/Unable to load URT::NonExistantClass while defining relationship prop/, 'Error message looks correct'); 52_limit_cache_size.t000444023532023421 1114212121654175 17007 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 23; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); #UR::DBI->monitor_sql(1); is(UR::Context->object_cache_size_highwater(50), 50, 'Set the max cache size to 50'); is(UR::Context->object_cache_size_lowwater(25), 25, 'Set the lowwater mark to 25'); # get a thing and hold onto a reference to it my $thing = URT::Thing->get(thing_id => 1); ok($thing, 'Got thing_id 1'); is( &count_things_in_cache(), 1, 'There is one object in the cache'); # We'll hold on to these, too my @keep_datas = $thing->datas(); is(scalar(@keep_datas), 2, 'Loaded 2 hangoff datas for that thing'); is( &count_things_in_cache(), 3, 'There are three objects in the cache'); my @things = URT::Thing->get(thing_id => { operator => '<=', value => '50'} ); is(scalar(@things), 50, 'Loaded 50 things with ID <= 50'); is(&count_things_in_cache('URT::Data'), 2, '2 URT::Datas are still in the cache'); is( &count_things_in_cache(), 52, 'There are 52 objects in the cache'); @things = URT::Thing->get(thing_id => { operator => '>', value => '80'} ); is(scalar(@things), 19, 'loaded 19 things with thing_id > 80'); is( &count_things_in_cache(), 22, 'The new 19 things, plus the original thing and 2 datas are still in the cache'); $thing = undef; is( &count_things_in_cache(), 21, 'After letting go of the original thing, there are now 21 objects in the cache'); $thing = URT::Thing->is_loaded(thing_id => 1); ok(!$thing, 'URT::Thing id 1 is no longer loaded'); @things = (); my @datas = URT::Data->get(id => { operator => '>', value => '80'}); is(scalar(@datas), 19, 'Loaded 19 datas with id > 80'); is(&count_things_in_cache('URT::Data'), 21, 'In total, there are 21 datas in the cache'); is(&count_things_in_cache('URT::Thing'), 19, 'Those 19 things are still loaded'); @datas = (); @keep_datas = (); is(&count_things_in_cache('URT::Data'), 19, 'After letting go of the original 2 datas, there are now 19 loaded'); $thing = URT::Thing->get(thing_id => 1); ok($thing, 're-got thing_id 1 after it was purged from the cache'); $thing = undef; @things = URT::Thing->get(); is(scalar(@things), 99, 'Got all URT::Things'); @things = (); &count_things_in_cache(); @datas = URT::Data->get(); is(scalar(@datas), 99, 'Got all URT::Datas'); @things = URT::Thing->is_loaded(); is(scalar(@things), 0, '0 URT::Things are loaded now'); @datas = (); &count_things_in_cache(); @things = URT::Thing->get(); is(scalar(@things), 99,'re-got all URT::Things after they were purged from the cache'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); # attribs belong to one thing $dbh->do("create table thing (thing_id integer, name varchar)"); $dbh->do("create table hangoff (id integer, data varchar, thing_id integer REFERENCES thing(thing_id))"); my $insert = $dbh->prepare("insert into thing (thing_id, name) values (?,?)"); for (my $i = 1; $i < 100; $i++) { $insert->execute($i, $i); } $insert->finish; # Two of these hangoffs will be related to one thing $insert = $dbh->prepare("insert into hangoff (id, data, thing_id) values (?,?,?)"); for (my $i = 1; $i < 100; $i++) { my $thing_id = int(($i+1)/2); $insert->execute($i, $i, $thing_id); } $insert->finish; UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, datas => { is => 'URT::Data', reverse_as => 'thing', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Data', id_by => 'id', has => [ data => { is => 'String' }, thing => { is => 'URT::Thing', id_by => 'thing_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'hangoff', ); } sub count_things_in_cache { my $count = 0; my @classes; if (@_) { @classes = @_; } else { @classes = ( 'URT::Thing', 'URT::Data' ); } foreach my $c ( @classes ) { my $this = scalar(values %{$UR::Context::all_objects_loaded->{$c}}); # print "Found $this $c objects\n"; foreach (values %{$UR::Context::all_objects_loaded->{$c}} ) { # print "\tid ",$_->id,"\n"; } $count += scalar(values %{$UR::Context::all_objects_loaded->{$c}}); } return $count; } 91c_set_relay.t000444023532023421 1263612121654175 15662 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 15; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, age integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table CAR_ENGINE (engine_id int NOT NULL PRIMARY KEY, car_id integer references CAR(car_id), size number)'), 'created car_engine table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, is_cool => { is => 'Boolean' }, age => { is => 'Integer' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, car_colors => { via => 'cars', to => 'color', is_many => 1 }, primary_car_color => { via => 'primary_car', to => 'color' }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'Number' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, engine => { is => 'URT::Car::Engine', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::Car::Engine', table_name => 'CAR_ENGINE', id_by => [ engine_id => { is => 'Number' }, ], has => [ size => { is => 'Number' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "created class for Engine"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?,?)'); foreach my $row ( [ 11, 'Bob',1, 25 ], [12, 'Fred',0, 30], [13, 'Mike',0, 35],[14,'Joe',1, 40], [15,'Frank', 1, 45] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 11], [ 2,'blue',1, 12], [3,'red',1,13],[4,'blue',1,14],[5,'yellow',1,11] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car_engine values (?,?,?)'); foreach my $row ( [100, 1, 350], [ 200, 2, 400], [300, 3, 428], [400, 4, 429], [500, 5, 289] ) { $insert->execute(@$row); } $insert->finish(); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'created a subscription for query'); #$DB::single = 1; my $bx1 = URT::Person->define_boolexpr( 'is_cool' => 1, 'cars.color' => 'red', 'cars.engine.size' => 428, 'cars.is_primary true' => 1, ); my $s1 = URT::Person->define_set($bx1); ok($s1, "made an initial set $s1"); my $bx1r1 = $bx1->reframe('primary_car'); my $s2 = $s1->primary_car_set; is($s2->id, $bx1r1->id, "the expected reframed id on related set $s2"); my $bx1r2 = $bx1->reframe('primary_car.engine'); my $s3 = $s2->engine_set; is($s3->id, $bx1r2->id, "the expected reframed id on related set $s3"); my $s5 = $s1->__related_set__('cars.engine'); is($s5->id, $s3->id, "reframed set two steps away persons's cars.engine"); my $s6 = $s5->car_set->owner_set; ok($s6, "went back from the engine set to the car to the owner"); is($s6->id, $s1->id, "the owner set from the engine matches the original"); #$DB::single = 1; my $bx4 = $s2->rule->reframe("color"); ok($bx4, "got color reframe $bx4"); my $bx7 = URT::Car::Engine->define_boolexpr('car.owner_id' => 1234); my $bx7r = $bx7->reframe('car.owner'); #$DB::single = 1; my $z1 = URT::Car->define_boolexpr("color" => "red"); print "$z1\n"; my $z2 = $z1->reframe("owner"); print "$z2\n"; my $z4 = $z1->reframe("engine"); print "$z4\n"; my $z3 = $z1->reframe("color"); print "$z3\n"; __END__ my $s4 = $s2->color_set(); ok($s4, "got a set of colors: $s4"); __END__ note("******** or *********"); my $bx5 = URT::Person->define_boolexpr( -or => [ ['is_cool' => 1], ['primary_car.color' => 'red'], ] ); ok($bx5, "created an 'or' boolexpr $bx5"); my $s5 = URT::Person->define_set($bx5); ok($s5, "made a set for it $s5"); is($s5->rule->id, $bx5->id, "id is correct"); my $bx6 = $bx5->reframe('primary_car'); ok($bx6, "$bx6"); my $s6 = $s5->primary_car_set(); ok($s6, "$s6"); is($s6->id, $bx6->id, "id is correct"); 37b_caching_with_in_clause.t000444023532023421 440712121654175 20320 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); ok($dbh->do('create table thing ( thing_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer, color varchar)'), 'created parent table'); ok(UR::Object::Type->define( class_name => 'URT::Thing', table_name => 'thing', id_by => [ 'thing_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, is_cool => { is => 'NUMBER' }, color => { is => 'STRING' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Thing"); my $sth = $dbh->prepare('insert into thing (thing_id, name, is_cool, color) values (?,?,?,?)'); ok($sth,'insert statement prepared'); foreach my $n ( 1 .. 10 ) { ok($sth->execute($n,$n,1,'green'), "inserted thing ID $n"); } $sth->execute(99,99,0,'white'); # item 99 is not cool my $load_count = 0; ok(URT::Thing->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Thing->get(name => [5,7,2,99,102], is_cool => 1); is(scalar(@o), 3, 'get() returned the correct number of items with in clause containing some non-matching values'); is($load_count, 3, 'loaded 0 new objects'); is($query_count, 1, 'made 1 query'); $load_count = 0; $query_count = 0; @o = URT::Thing->get(name => 5, is_cool => 1, color => 'green'); is(scalar(@o), 1, 'get() correctly returns object matching name that was in the previous in-clause'); is($load_count, 0, 'loaded 0 new objects'); is($query_count, 0, 'no query was generated'); 21c_load_observer_abstract_parent.t000444023532023421 1021312121654175 21733 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 22; # Make an abstract class with a table, and a child class with no table of its own. # The 'load' signal should only ever be fired once for each object loaded my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('CREATE TABLE person (person_id integer, name varchar, subclass_name varchar)'), 'create table'); ok($dbh->do("INSERT into person VALUES (1, 'Bob', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (2, 'Fred', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (3, 'Joe', 'URT::Employee')"), 'insert into person table'); ok($dbh->do("INSERT into person VALUES (4, 'Mike', 'URT::Employee')"), 'insert into person table'); UR::Object::Type->define( class_name => 'URT::Person', is_abstract => 1, subclassify_by => 'subclass_name', id_by => 'person_id', has => [ name => { is => 'String' }, subclass_name => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Employee', is => 'URT::Person', ); my @person_observations; my $person_obv = URT::Person->add_observer(callback => sub { my($obj,$method) = @_; push @person_observations, [$method, $obj->class, $obj->id]; #print "*** Got $method signal for obj ".$obj->id." named ".$obj->name." in class ".$obj->class."\n"; #print Carp::longmess(); }); ok($person_obv, "made an observer on Person class"); my @employee_observations; my $employee_obv = URT::Employee->add_observer(callback => sub { my($obj,$method) = @_; push @employee_observations, [$method, $obj->class, $obj->id]; }); ok($employee_obv, "made an observer on Employee class"); @person_observations = (); @employee_observations = (); my $person = URT::Person->get(1); ok($person, 'Got person ID 1'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 1] ], # subclasses/loaded as Employee 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 1] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); $person = URT::Employee->get(2); ok($person, 'Got Employee ID 2'); is(scalar(@person_observations), 1, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 2] ], 'Person observations match expected'); is(scalar(@employee_observations), 1, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ [ 'load', 'URT::Employee', 2] ], 'Employee observations match expected'); @person_observations = (); @employee_observations = (); my @people = URT::Person->get(); is(scalar(@people), 4, 'Got 4 Person objects'); is(scalar(@person_observations), 2, 'Saw correct number of Person observations'); is_deeply(\@person_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Person observations match expected'); is(scalar(@employee_observations), 2, 'Saw correct number of Employee observations'); is_deeply(\@employee_observations, [ ['load', 'URT::Employee', 3], ['load', 'URT::Employee', 4] ], 'Employee observations match expected'); 29_indirect_calculated_accessor.t000444023532023421 1051312121654175 21365 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 14; ok(setup(), 'Create initial schema, data and classes'); my $boss = URT::Boss->get(1); ok($boss, 'Got boss id 1'); is($boss->full_name, 'Bob Smith', "Boss' full name is correct"); is($boss->upper_first_name, 'BOB', "Boss' first name in all caps (presumedly from SQL)"); my $empl = URT::Employee->get(name => 'Joe'); ok($empl, 'Got an employee'); is($empl->boss_name, 'Bob Smith', "Employee's boss' name is correct"); is($empl->boss_upper_first_name, 'BOB', "Employee's boss' first name in all caps"); $empl = URT::Employee->get(name => 'Foo'); ok($empl, 'Got another employee with a different boss not yet loaded'); is($empl->boss_name, 'Fred Jones', "Employee's boss' name is correct"); is($empl->boss_upper_first_name, 'FRED', "Employee's boss' first name in all caps"); my @e = $boss->employees(); is(scalar(@e),2, "big boss has one employee plus himself"); my $boss2 = URT::Boss->get(2); @e = $boss2->employees(); is(scalar(@e),3, "middle manager has three employees"); @e = $boss2->secret_employees(); is(scalar(@e),2, "middle manager has two secret employees"); ok(cleanup(), 'Removed schema'); # define the data source, create a table and classes for it sub setup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle || return; $dbh->do('create table if not exists BOSS (boss_id int, first_name varchar, last_name varchar, company varchar)') || return; $dbh->do('create table if not exists EMPLOYEE (emp_id int, name varchar, is_secret boolean, boss_id int CONSTRAINT boss_fk references BOSS(BOSS_ID))') || return; my $boss_sth = $dbh->prepare('insert into BOSS (boss_id, first_name, last_name, company) values (?,?,?,?)') || return; $boss_sth->execute(1, 'Bob', 'Smith', 'CoolCo') || return; $boss_sth->execute(2, 'Fred', 'Jones', 'Data Inc') || return; $boss_sth->finish(); my $employee_sth = $dbh->prepare('insert into EMPLOYEE (emp_id, name, boss_id, is_secret) values (?,?,?,?)') || return; $employee_sth->execute(1,'Joe', 1, 0) || return; $employee_sth->execute(2,'Mike', 1, 0) || return; $employee_sth->execute(3,'Foo', 2, 1) || return; $employee_sth->execute(4,'Bar', 2, 0) || return; $employee_sth->execute(5,'Baz', 2, 1) || return; $dbh->commit() || return; UR::Object::Type->define( class_name => "URT::Boss", id_by => 'boss_id', has => [ boss_id => { type => "Number" }, first_name => { type => "String" }, last_name => { type => "String" }, full_name => { calculate_from => ['first_name','last_name'], calculate => '$first_name . " " . $last_name', }, upper_first_name => { calculate_from => 'first_name', calculate_sql => 'upper(first_name)' }, company => { type => "String" }, employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss' }, secret_employees => { is => 'URT::Employee', is_many => 1, reverse_as => 'boss', where => [is_secret => 1] }, ], table_name => 'BOSS', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Employee', id_by => 'emp_id', has => [ emp_id => { type => "Number" }, name => { type => "String" }, is_secret => { is => 'Boolean' }, boss_id => { type => 'Number'}, boss => { type => "URT::Boss", id_by => 'boss_id' }, boss_name => { via => 'boss', to => 'full_name' }, boss_upper_first_name => { via => 'boss', to => 'upper_first_name' }, company => { via => 'boss' }, ], table_name => 'EMPLOYEE', data_source => 'URT::DataSource::SomeSQLite', ); return 1; } sub cleanup { my $dbh = URT::DataSource::SomeSQLite->get_default_handle || return; $dbh->do('drop table BOSS') || return; $dbh->do('drop table EMPLOYEE') || return; return 1; } 87b_is_many_id_class_by_is_efficient.t000444023532023421 1222312121654175 22370 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # This test tries getting a property delegated through an object accessor # with an id_class_by, effectively making it doubly-delegated # # In this situation, the accessor should collect the bridge objects # (Inventory in this test), bucket them by final result class, and # then do a single get() for each result class with the IDs of # the result items collected from the bridge objects use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar)'), 'created person table'); ok($dbh->do('create table INVENTORY ( inv_id int NOT NULL PRIMARY KEY, owner_id integer, value_id varchar, value_class varchar, category varchar)'), 'created inventory table'); ok($dbh->do('create table PROPERTY ( property_id int NOT NULL PRIMARY KEY, name varchar, size integer)'), 'created item table'); ok($dbh->do('create table ITEM ( item_id int NOT NULL PRIMARY KEY, name varchar, size integer)'), 'created item table'); UR::Object::Type->define( class_name => 'URT::OwnedThing', is_abstract => 1, ); UR::Object::Type->define( class_name => 'URT::Property', is => 'URT::OwnedThing', doc => 'Things someone can own that has a record of title', id_by => 'property_id', has => ['name','size'], table_name => 'PROPERTY', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Item', is => 'URT::OwnedThing', doc => 'Things someone can own that has no record of title', id_by => 'item_id', has => ['name','size'], table_name => 'ITEM', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => 'person_id', has => [ name => { is => 'String' }, ], has_many => [ inventory => { is => 'URT::Inventory', reverse_as => 'owner' }, vehicles => { is => 'URT::Property', via => 'inventory', to => 'thing', where => [category => 'vehicles'] }, money => { is => 'URT::Item', via => 'inventory', to => 'thing', where => [category => 'money'] }, things => { is => 'URT::OwnedItem', via => 'inventory', to => 'thing' }, ], table_name => 'PERSON', data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'URT::Inventory', id_by => 'inv_id', has => [ category => { is => 'String' }, thing => { is => 'URT::OwnedThing', id_by => 'value_id', id_class_by => 'value_class' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, ], table_name => 'INVENTORY', data_source => 'URT::DataSource::SomeSQLite', ); # Insert some data # Bob has 2 cars, a house, 3 pieces of money and a dog # Fred has 1 car, 1 snowmobile and a cat my $insert = $dbh->prepare('insert into person values (?,?)'); foreach my $row ( [ 1, 'Bob'], [2,'Fred'] ) { $insert->execute(@$row); } $insert->finish; $insert = $dbh->prepare('insert into item values (?,?,?)'); foreach my $row ( [ 1, 'coin', 1], [2, 'dollar', 2], [3, 'coin', 1], [4, 'dog', 10], [ 5, 'cat', 8], ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into property values (?,?,?)'); foreach my $row ( [ 1, 'blue car', 100], [2, 'house', 1000], [3, 'red car', 200], [ 4, 'yellow car', 100], [5, 'snowmobile', 50], ) { $insert->execute(@$row); } $insert->finish(); # id, owner_id, value_id, value_class, category $insert = $dbh->prepare('insert into inventory values (?,?,?,?,?)'); foreach my $row ( [1, 1, 1, 'URT::Item', 'money'], [2, 1, 2, 'URT::Item', 'money'], [3, 1, 3, 'URT::Item', 'money'], [4, 1, 4, 'URT::Item', 'livestock'], [5, 1, 1, 'URT::Property', 'vehicles'], [6, 1, 2, 'URT::Property', 'land'], [7, 1, 3, 'URT::Property', 'vehicles'], [8, 2, 5, 'URT::Item', 'livestock'], [9, 2, 4, 'URT::Property', 'vehicles'], [10, 2, 5, 'URT::Property', 'vehicles'], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $person = URT::Person->get(1); ok($person, 'Got person object'); $query_count = 0; my @money = $person->money(); is(scalar(@money), 3, 'person has 3 pieces of money'); is($query_count, 2, 'made 2 queries'); # 1 for the inventory bridges and 1 for all the money items $person = URT::Person->get(2); ok($person, 'Got a different person'); $query_count = 0; my @things = $person->things(); is(@things, 3, 'Second person has 3 things'); is($query_count, 3, 'Made 3 queries'); # 1 for the inventory bridges, 1 for the Items and 1 for the Propertys 05_get_create_get.t000444023532023421 376712121654175 16451 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; &create_tables_and_classes(); my $p1 = URT::Product->get(1); ok(!$p1, 'Get by non-existent ID correctly returns nothing'); my $p2 = URT::Product->create(id => 1, name => 'jet pack', genius => 6, manufacturer_name => 'Lockheed Martin',sc => 'URT::TheSubclass'); ok($p2, 'Create a new Product with the same ID'); $p1 = URT::Product->get(1); ok($p1, 'Get with the same ID returns something, now'); is($p1->id, 1, 'ID is correct'); is($p1->name, 'jet pack', 'name is correct'); is($p1->genius, 6, 'name is correct'); is($p1->manufacturer_name, 'Lockheed Martin', 'name is correct'); my @prods = URT::Product->get('genius between' => [1,10]); is(scalar(@prods), 1, 'get() with between works'); sub create_tables_and_classes { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PRODUCT ( prod_id int NOT NULL PRIMARY KEY, name varchar, genius integer, manufacturer_name varchar, sc varchar)'), 'created product table'); ok(UR::Object::Type->define( class_name => 'URT::Product', table_name => 'PRODUCT', is_abstract => 1, id_by => [ prod_id => { is => 'NUMBER' }, ], has => [ name => { is => 'STRING' }, genius => { is => 'NUMBER' }, manufacturer_name => { is => 'STRING' }, sc => { is => 'String' }, ], subclassify_by => 'sc', data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Product"); ok(UR::Object::Type->define( class_name => 'URT::TheSubclass', is => 'URT::Product', ), "Created class for TheSubclass"); } 04f_filemux.t000444023532023421 1101412121654175 15326 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 37; use IO::File; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace use URT::DataSource::SomeFileMux; &setup_files_and_classes(); my $obj = URT::Thing->get(thing_id => 1, thing_type => 'person'); ok($obj, 'Got a person thing with id 1'); is($obj->thing_name, 'Joel', 'Name is correct'); is($obj->thing_color, 'grey', 'Color is correct'); is($obj->thing_type, 'person', 'type is correct'); $obj = URT::Thing->get(thing_id => 6, thing_type => 'robot'); ok($obj, 'Got a robot thing with id 5'); is($obj->thing_name, 'Tom', 'Name is correct'); is($obj->thing_color, 'red', 'Color is correct'); $obj = URT::Thing->get(thing_id => 3, thing_type => 'person'); ok(!$obj, 'Correctly found no person thing with id 3'); my @objs = URT::Thing->get(thing_type => ['person','robot'], thing_id => 7); is(scalar(@objs),1, 'retrieved a thing with id 7 that is either a person or robot'); is($objs[0]->thing_id, 7, 'The retrieved thing has the right id'); is($objs[0]->thing_type, 'robot', 'The retrieved thing is a robot'); is($objs[0]->thing_name, 'Gypsy', 'Name is correct'); is($objs[0]->thing_color, 'purple', 'Color is correct'); my $filemux_error_message; URT::DataSource::SomeFileMux->error_messages_callback(sub { $filemux_error_message = $_[1]; $_[1] = undef }); $obj = eval { URT::Thing->get(thing_id => 2) }; ok(!$obj, "Correctly couldn't retrieve a Thing without a thing_type"); like($filemux_error_message, qr(Recursive entry.*URT::Thing), 'Error message did mention recursive call trapped'); my $iter = URT::Thing->create_iterator(thing_type => ['person', 'robot']); ok($iter, 'Created an iterator for all Things'); my $expected_id = 1; while (my $obj = $iter->next()) { ok($obj, 'Got an object from the iterator'); is($obj->id, $expected_id++, 'Its ID was the expected value'); } # Try the object pruner to unload the File data sources my @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 2, 'Two file data sources were defined'); @file_data_sources = (); my @warnings; { my @warnings = (); local $SIG{'__WARN__'} = sub { push @warnings, @_ }; UR::Context->object_cache_size_lowwater(1); UR::Context->object_cache_size_highwater(2); ok(UR::Context->current->prune_object_cache(), 'Force object cache pruning'); } @warnings = grep { $_ !~ m/After seceral passes of pruning the object cache, there are still \d+ objects/ } @warnings; is(scalar(@warnings), 0, 'No unexpected warnings from pruning'); UR::Context->object_cache_size_lowwater(undef); UR::Context->object_cache_size_highwater(undef); @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 0, 'After cache pruning, no file data sources are defined'); if (@file_data_sources) { foreach (@file_data_sources) { print STDERR Data::Dumper::Dumper($_); } } # try getting something again, should re-create the data source object $obj = UR::Context->current->reload('URT::Thing', thing_type => 'person', thing_id => 1); ok($obj, 'Reloading URT::Thing id 3'); @file_data_sources = UR::DataSource::File->is_loaded(); is(scalar(@file_data_sources), 1, 'The File data source was re-created'); sub setup_files_and_classes { my $dir = $URT::DataSource::SomeFileMux::BASE_PATH; my $delimiter = URT::DataSource::SomeFileMux->delimiter; my $file = "$dir/person"; my $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(1 Joel grey)),"\n"); $f->print(join($delimiter, qw(2 Mike blue)),"\n"); $f->print(join($delimiter, qw(4 Frank black)),"\n"); $f->print(join($delimiter, qw(5 Clayton green)),"\n"); $f->close(); $file = "$dir/robot"; $f = IO::File->new(">$file") || die "Can't open $file for writing: $!"; $f->print(join($delimiter, qw(3 Crow gold)),"\n"); $f->print(join($delimiter, qw(6 Tom red)),"\n"); $f->print(join($delimiter, qw(7 Gypsy purple)),"\n"); $f->close(); my $c = UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Integer' }, ], has => [ thing_name => { is => 'String' }, thing_color => { is => 'String' }, thing_type => { is => 'String', valid_values => ['person', 'robot'] }, ], table_name => 'wefwef', data_source => 'URT::DataSource::SomeFileMux', ); ok($c, 'Created class'); } 54_valid_values.t000444023532023421 761612121654175 16167 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 18; class Game::Card { has => [ suit => { is => 'Text', valid_values => [qw/heart diamond club spade/], }, color => { is => 'Text', valid_values => [qw/red blue green/], is_mutable => 0 }, owner => { is => 'Text', is_optional => 1 }, pips => { is => 'Number', is_optional => 0 }, ], }; for my $class (qw/Game::Card/) { my $c1 = $class->create(suit => 'spade', color => 'red', pips => 4); ok($c1, "created an object with a valid property"); my @i1 = $c1->__errors__; is(scalar(@i1), 0, "no cases of invalididy") or diag(Data::Dumper::Dumper(\@i1)); my $c2 = $class->create(suit => 'badsuit', color => 'blue', pips => 9); ok($c2, "created an object with an invalid property"); my @i2 = $c2->__errors__; is(scalar(@i2), 1, "one expected cases of invalididy") or diag(Data::Dumper::Dumper(\@i2)); is($i2[0]->__display_name__, qq(INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade), 'Error text is corect'); $c2->suit('heart'); @i2 = $c2->__errors__; is(scalar(@i2), 0, "zero cases of invalididy after fix") or diag(Data::Dumper::Dumper(\@i2)); my $c3 = $class->create(suit => 'spade', color => 'red'); ok($c3, 'Created color with missing required param'); my @i3 = $c3->__errors__; is(scalar(@i3), 1, 'one expected cases of invalididy') or diag(Data::Dumper::Dumper(\@i3)); is($i3[0]->__display_name__, qq(INVALID: property 'pips': No value specified for required property), 'Error text is corect'); my $c4 = $class->create(suit => 'badsuit', color => 'blue'); ok($c4, 'Created object with invalid property value and missing required param'); my @i4 = sort { $a->__display_name__ cmp $b->__display_name__ } $c4->__errors__; is(scalar(@i4), 2, 'two expected cases of invalididy') or diag(Data::Dumper::Dumper(\@i4)); is($i4[0]->__display_name__, qq(INVALID: property 'pips': No value specified for required property), 'First error text is corect'); is($i4[1]->__display_name__, qq(INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade), 'second error text is corect'); my $context = UR::Context->current; $context->dump_error_messages(0); $context->queue_error_messages(1); ok(!UR::Context->commit, 'Commit fails as expected'); my @error_messages = sort {$a cmp $b } UR::Context->current->error_messages(); is(scalar(@error_messages), 3, 'commit generated 3 error messages'); is($error_messages[2], # This one prints first, but sorts 3rd 'Invalid data for save!', 'First error message is correct'); my $c3_id = $c3->id; like($error_messages[0], qr/Game::Card identified by $c3_id has problems on\s+INVALID: property 'pips': No value specified for required property\s+Current state:\s+\$VAR1 = bless\( {/s, 'Second error message is correct'); my $c4_id = $c4->id; like($error_messages[1], qr/Game::Card identified by $c4_id has problems on\s+INVALID: property 'pips': No value specified for required property\s+INVALID: property 'suit': The value badsuit is not in the list of valid values for suit. Valid values are: heart, diamond, club, spade\s+Current state:\s+\$VAR1 = bless\( {/s, 'Third error message is correct'); #my $c5 = eval { $class->create(suit => 'spade', color => 'badcolor') }; #ok(!defined($c5), "correctly refused to create an object with an invalid immutable property") # or diag(Data::Dumper::Dumper($c5)); } 63c_view_with_subviews.t.expected.person.json000444023532023421 56512121654175 23641 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t{ "age" : "99 yrs", "cats" : [ { "age" : "2 yrs", "fluf" : "11", "name" : "fluffy", "owner" : { "id" : "111" } }, { "age" : "8 yrs", "fluf" : "22 yrs", "name" : "nestor", "owner" : { "id" : "111" } } ], "name" : "Fester" } 49g_complicated_get_double_join.t000444023532023421 414512121654175 21352 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 1; # This tests a get() where the same tabe/column (attribute.value) is getting filtered with # diggerent values as a result of two different properties (name and sibling_name) # # The SQL writer was getting confused by the time it got to the WHERE clause, and # applied them both the whatever alias was used for the final join to that table my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table person (person_id integer PRIMARY KEY NOT NULL, sibling_id integer)'); $dbh->do('create table attribute (person_id integer REFERENCES person(person_id), key varchar NOT NULL, value varchar, PRIMARY KEY (person_id, key))'); # Make 2 people named Bob and Fred, they are siblings $dbh->do("insert into person values (1, 2)"); $dbh->do("insert into attribute values (1,'name','Bob')"); $dbh->do("insert into person values (2, 1)"); $dbh->do("insert into attribute values (2,'name','Fred')"); UR::Object::Type->define( class_name => 'Person', table_name => 'person', id_by => [ person_id => { is => 'integer' }, ], has => [ attributes => { is => 'Attribute', reverse_as => 'person', is_many => 1 }, name => { is => 'String', via => 'attributes', where => [key => 'name'], to => 'value' }, sibling => { is => 'Person', id_by => 'sibling_id' }, sibling_name => { via => 'sibling', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', ); UR::Object::Type->define( class_name => 'Attribute', table_name => 'attribute', data_source => 'URT::DataSource::SomeSQLite', id_by => [ person_id => { is => 'Integer' }, key => { is => 'String' }, ], has => [ person => { is => 'Person', id_by => 'person_id'}, value => { is => 'String' }, ], ); my @p = Person->get(name => 'Bob', sibling_name => 'Fred' ); is(scalar(@p), 1, 'Got one person'); 28_dont_index_delegated_props.t000444023532023421 276112121654175 21062 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 7; UR::Object::Type->define( class_name => 'Person', has => [ name => { is => 'String' }, attribs => { is => 'PersonAttr', is_many => 1, reverse_as => 'person' }, address => { is => 'String', via => 'attribs', to => 'value', where => [key => 'address'] }, ], ); UR::Object::Type->define( class_name => 'PersonAttr', has => [ person => { is => 'Person', id_by => 'person_id' }, key => { is => 'String' }, value => { is => 'String' }, ], ); my $bob = Person->create(name => 'Bob'); my $bob_addr = $bob->add_attrib(key => 'address', value => '123 main st'); my $fred = Person->create(name => 'Fred'); my $fred_addr = $fred->add_attrib(key => 'address', value => '456 oak st'); my @people = Person->get(name => 'Fred'); is(scalar(@people), 1, 'Got 1 person named Fred'); is($people[0], $fred, 'it is the right person'); @people = Person->get(address => '123 main st'); is(scalar(@people), 1, 'Got 1 person with address 123 main st'); is($people[0], $bob, 'it is the right person'); ok($fred_addr->value('789 elm st'), 'Change address for Fred'); @people = Person->get(address => '456 oak st'); is(scalar(@people), 0, 'Got 0 people at Fred\' old address'); is($fred->address, '789 elm st', 'Address for Fred is correct through delegated property'); 68_trapped_death_does_not_stack_trace.t000444023532023421 103012121654175 22537 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; $ENV{UR_STACK_DUMP_ON_DIE}=1; sub a { b(@_) } sub b { c(@_) } sub c { d(@_) } sub d { die 'expected' } eval { &a }; if ($@) { note $@; if ($@ =~ /main::b\(\) called/g) { fail('got a stack trace in eval'); } else { pass('looks good'); } } else { fail('$@ wasnt set'); } #&a; 49m_reverse_as_is_delegated.t000444023532023421 1115412121654175 20531 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use URT::DataSource::SomeSQLite; use Test::More tests => 19; # This test does a query that joins three tables. # The get() is done on an is-many property, and its reverse_as is a delegated # property through a third class my $dbh = URT::DataSource::SomeSQLite->get_default_handle; $dbh->do('create table car (car_id integer not null primary key, model varchar not null)'); $dbh->do('create table driver (driver_id integer not null primary key, name varchar not null)'); $dbh->do('create table car_driver (car_id integer not null references car(car_id), driver_id integer not null references driver(driver_id))'); $dbh->do("insert into car values (1,'batmobile')"); $dbh->do("insert into car values (2,'race car')"); $dbh->do("insert into car values (3,'mach 5')"); $dbh->do("insert into car values (4,'junked car')"); $dbh->do("insert into driver values (1,'batman')"); $dbh->do("insert into driver values (2,'mario')"); $dbh->do("insert into driver values (3,'speed racer')"); $dbh->do("insert into driver values (4,'superman')"); # batman drives the batmobile $dbh->do("insert into car_driver values (1,1)"); # mario and speed racer drive the race car $dbh->do("insert into car_driver values (2,2)"); $dbh->do("insert into car_driver values (2,3)"); # speed racer also drives the mach 5 $dbh->do("insert into car_driver values (3,3)"); # superman doesn't drive anything # no one drives the junked car UR::Object::Type->define( class_name => 'URT::Car', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', id_by => [ car_id => { is => 'Integer' }, ], has => [ model => { is => 'String', }, ], has_many => [ car_drivers => { is => 'URT::CarDriver', reverse_as => 'car' }, drivers => { is => 'URT::Driver', via => 'car_drivers', to => 'driver' }, # regular many-to-many property def'n driver_names => { is => 'String', via => 'drivers', to => 'name' }, ], ); UR::Object::Type->define( class_name => 'URT::Driver', data_source => 'URT::DataSource::SomeSQLite', table_name => 'driver', id_by => [ driver_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, ], has_many => [ cars => { is => 'URT::Car', reverse_as => 'drivers' }, # not the usual way to make a many-to-many property def'n car_models => { is => 'String', via => 'cars', to => 'model' }, ], ); UR::Object::Type->define( class_name => 'URT::CarDriver', data_source => 'URT::DataSource::SomeSQLite', table_name => 'car_driver', id_by => [ car_id => { is => 'Integer' }, driver_id => { is => 'Integer' }, ], has => [ car => { is => 'URT::Car', id_by => 'car_id' }, driver => { is => 'URT::Driver', id_by => 'driver_id' }, ], ); my $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub { $query_count++ }), 'Created a subscription for query'); $query_count = 0; my $driver = URT::Driver->get(name => 'batman'); ok($driver, 'got the batman driver'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @cars = $driver->cars(); is(scalar(@cars), 1, 'batman drives 1 car'); is($query_count, 1, 'Made 1 query'); is($cars[0]->model, 'batmobile', 'It is the right car'); $query_count = 0; @cars = $driver->cars(); is(scalar(@cars), 1, 'trying again, batman drives 1 car'); TODO: { local $TODO = "query cache doesn't track properties like drivers.id"; is($query_count, 0, 'Made no queries'); } is($cars[0]->model, 'batmobile', 'It is the right car'); $query_count = 0; my @models = $driver->car_models(); is(scalar(@models), 1, 'batman has 1 car model'); is_deeply(\@models, ['batmobile'], 'Got the right car'); is($query_count, 0, 'Made 0 queries'); $driver = URT::Driver->get(name => 'speed racer'); ok($driver, 'Got speed racer'); $query_count = 0; @models = $driver->car_models(); is(scalar(@models), 2, 'speed racer drives 2 cars'); @models = sort @models; is_deeply(\@models, ['mach 5', 'race car'], 'Got the right cars'); is($query_count, 1, 'Made 1 query'); $driver = URT::Driver->get(name => 'superman'); ok($driver, 'Got superman'); $query_count = 0; @models = $driver->car_models(); is(scalar(@models), 0, 'superman drives 0 cars'); TODO: { local $TODO = "UR::BX::Template->resolve needs to support meta opt -hints to make this work"; is($query_count, 1, 'Made 1 query'); } 96c_ur_context_current_and_process.t000444023532023421 134612121654175 22170 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More; __PACKAGE__->main('UR'); sub main { my ($test, $module) = @_; use_ok($module) or exit; $test->ur_context_process_is_distinguished_from_current; done_testing(); } sub ur_context_process_is_distinguished_from_current { my $self = shift; my $cc = UR::Context->current; my $cp = UR::Context->process; is($cc->id, $cp->id, 'current returned the same as process'); my $tx = UR::Context::Transaction->begin; my $new_cc = UR::Context->current; my $new_cp = UR::Context->process; isnt($new_cc->id, $cc->id, 'current changed within transaction'); is ($new_cp->id, $cp->id, 'process did not change within transaction'); } 17c_rw_property_alias.t000444023532023421 1233512121654175 17432 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 42; use URT::DataSource::SomeSQLite; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table person (person_id integer PRIMARY KEY, name varchar NOT NULL)"), 'Created person table'); ok( $dbh->do("create table car (car_id integer PRIMARY KEY, make varchar NOT NULL, owner_id integer NOT NULL REFERENCES person(person_id))"), 'Created car table'); ok( $dbh->do("insert into person values(1, 'Henry')"), 'Insert person 1'); ok( $dbh->do("insert into person values(2, 'Louis')"), 'Insert person 2'); ok( $dbh->do("insert into person values(3, 'Walter')"), 'Insert person 3'); ok( $dbh->do("insert into person values(4, 'Frederick')"), 'Insert person 4'); ok( $dbh->do("insert into car values(1, 'Ford', 1)"), 'Insert car 1'); ok( $dbh->do("insert into car values(2, 'GM', 2)"), 'Insert car 2'); ok( $dbh->do("insert into car values(3, 'Chrysler', 3)"), 'Insert car 3'); ok( $dbh->do("insert into car values(4, 'Duesenberg', 4)"), 'Insert car 4'); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Person', id_by => [ 'person_id' ], has => [ 'name' => { is => 'String' }, 'mark' => { via => '__self__', to => 'name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'person', ); UR::Object::Type->define( class_name => 'URT::Car', id_by => [ car_id => { is => 'Integer' }, ], has_mutable => [ make => { is => 'UR::Value::Text' }, manufacturer => { is => 'String', via => '__self__', to => 'make' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, titleholder => { via => '__self__', to => 'owner' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'car', ); # Try calling the alias methods my $car = URT::Car->get(manufacturer => 'GM'); ok($car, 'Got car 2 filtered by manufacturer'); is($car->id, 2, 'It is the correct car'); $car = URT::Car->get(make => 'Ford'); ok($car, 'Got car 1 via "make"'); my $another_car = URT::Car->get(manufacturer => 'Ford'); ok($another_car, 'Got car 1 via "manufacturer'); is($car, $another_car, 'They are the same car'); ok($car->make('Honda'), 'Change make'); is($car->make, 'Honda', '"make" is updated'); is($car->manufacturer, 'Honda', '"manufacturer" is the same'); ok($car->manufacturer('Toyota'), 'Change manufacturer'); is($car->make, 'Toyota', '"make" is updated'); is($car->manufacturer, 'Toyota', '"manufacturer" is the same'); # Try querying by different kinds of properties $car = URT::Car->get('owner.name' => 'Walter'); ok($car, 'Got a car via owner.name'); is($car->make, 'Chrysler', 'It is the right car'); $car = URT::Car->get('titleholder.mark', 'Frederick'); ok($car, 'Got a car via titleholder.mark'); is($car->make, 'Duesenberg', 'It is the right car'); # Try creating something new my $bmw_car = URT::Car->create(id => 10, make => 'BMW', owner_id => 1); ok($bmw_car, 'Created new car with "make"'); is($bmw_car->make, 'BMW', '"make" returns correct value'); is($bmw_car->manufacturer, 'BMW', '"manufacturer" returns correct value'); my $audi_car = URT::Car->create(id => 11, manufacturer => 'Audi', owner_id => 1); ok($audi_car, 'Created new car with "manufacturer"'); is($audi_car->make, 'Audi', '"make" returns correct value'); is($audi_car->manufacturer, 'Audi', '"manufacturer" returns correct value'); ok(UR::Context->commit(), 'Commit changes'); my $sth = $dbh->prepare('select * from car'); $sth->execute(); my $results = $sth->fetchall_hashref('car_id'); is_deeply($results, { 1 => { car_id => 1, make => 'Toyota', owner_id => 1 }, 2 => { car_id => 2, make => 'GM', owner_id => 2 }, 3 => { car_id => 3, make => 'Chrysler', owner_id => 3 }, 4 => { car_id => 4, make => 'Duesenberg', owner_id => 4 }, 10 => { car_id => 10, make => 'BMW', owner_id => 1 }, 11 => { car_id => 11, make => 'Audi', owner_id => 1 } }, 'Data was saved to the DB properly'); # Try with some non-standard property definitions UR::Object::Type->define( class_name => 'URT::Owner', id_by => 'owner_id', has => ['name'], ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String'}, owner => { is => 'URT::Owner' }, # no id_by titleholder => { via => '__self__', to => 'owner' }, ], ); my $owner = URT::Owner->create(name => 'Bob'); ok($owner, 'Created an Owner'); my $thing = URT::Thing->create(name => 'Thingy'); ok($thing, 'Created a Thing'); ok($thing->owner($owner), 'Assigned an owner to the thing'); # The next get() will generate an error message, suppress it URT::Thing->__meta__->property('owner')->dump_error_messages(0); my $thing2 = URT::Thing->get('owner.name' => 'Bob'); ok($thing2, 'Got a thing via owner.name'); is($thing2->id, $thing->id, 'It is the right Thing'); $thing2 = URT::Thing->get('titleholder.name' => 'Bob'); ok($thing2, 'Got a thing via titleholder.name'); is($thing2->id, $thing->id, 'It is the right Thing'); 55b_partial_metada_data.t000444023532023421 744312121654175 17612 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; # This test assummes the storage DB schema already exists, but that the metaDB has # incomplete or outdated info about it, though the class and actual DB schema do match plan tests => 26; my $dbh = URT::DataSource::SomeSQLite->get_default_handle(); ok($dbh->do('create table TABLE_A (a_id integer PRIMARY KEY, value1 varchar, value2 varchar)'), 'Create table'); ok($dbh->do("insert into TABLE_A values (1,'hello','there')"), 'insert row 1'); ok($dbh->do("insert into TABLE_A values (2,'goodbye','cruel world')"), 'insert row 2'); ok(UR::Object::Type->define( class_name => 'URT::A', id_by => 'a_id', has => ['value1','value2'], data_source => 'URT::DataSource::SomeSQLite', table_name => 'TABLE_A', ), 'Define class A'); # Fab up metaDB info, but leave out the value2 column my %table_info = ( data_source => 'URT::DataSource::SomeSQLite', owner => 'main', table_name => 'TABLE_A'); #my %table_info = ( data_source => 'URT', owner => 'main', table_name => 'TABLE_A'); ok(UR::DataSource::RDBMS::Table->__define__(%table_info, last_object_revision => time(), er_type => 'entity', table_type => 'table'), 'Make table metadata obj'); ok(UR::DataSource::RDBMS::TableColumn->__define__(%table_info, last_object_revision => time(), column_name => 'a_id', data_type => 'integer', nullable => 'N'), 'Make column metadata obj for a_id'); ok(UR::DataSource::RDBMS::TableColumn->__define__(%table_info, last_object_revision => time(), column_name => 'value1', data_type => 'varchar', nullable => 'Y'), 'Make column metadata obj for value1'); ok(UR::DataSource::RDBMS::PkConstraintColumn->__define__(%table_info, column_name => 'a_id', rank => 0), 'Make Pk constraint metadata obj for a_id'); my $obj = URT::A->get(1); ok($obj, 'Got object with ID 1'); my %values = ( a_id => 1, value1 => 'hello', value2 => 'there'); foreach my $key ( keys %values ) { is($obj->$key, $values{$key}, "$key property is correct"); } ok($obj->value2('gracie'), 'Change value for value2'); $obj = URT::A->get(2); ok($obj, 'Got object with ID 2'); %values = ( a_id => 2, value1 => 'goodbye', value2 => 'cruel world'); foreach my $key ( keys %values ) { is($obj->$key, $values{$key}, "$key property is correct"); } ok($obj->delete, 'Delete object ID 2'); $obj = URT::A->create(a_id => 3, value1 => 'it', value2 => 'works'); ok($obj, 'Created a new object'); ok(UR::Context->current->commit, 'Commit'); my $sth = $dbh->prepare('select * from table_a where a_id = ?'); ok($sth, 'Make statement handle for checking data'); $sth->execute(1); my $objdata = $sth->fetchrow_hashref(); ok($objdata, 'Got data for a_id == 1'); is_deeply($objdata, { a_id => 1, value1 => 'hello', value2 => 'gracie'}, 'Saved data is correct'); $sth->execute(2); $objdata = $sth->fetchrow_hashref(); ok(!$objdata, 'Data for a_id == 2 was deleted'); $sth->execute(3); $objdata = $sth->fetchrow_hashref(); ok($objdata, 'Got data for a_id == 3'); is_deeply($objdata, { a_id => 3, value1 => 'it', value2 => 'works'}, 'Saved data is correct'); 79_like_operator.t000444023532023421 307312121654175 16350 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 9; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my $thing = URT::Thing->get('value like' => '%One'); ok($thing, "Loaded thing iwth 'value like' => '%One'"); is($thing->id, 1, 'It was the right thing'); my @things = URT::Thing->get('value not like' => '%Two'); is(scalar(@things), 4, "Loaded 4 things with 'value not like' => '%Two'"); @things = URT::Thing->get('value like' => 'Number%'); is(scalar(@things), 5, "Got 5 things with 'value like' => 'Number%'"); @things = URT::Thing->get('value not like' => '%blah%'); is(scalar(@things), 5, "Got 5 things with 'value not like' => '%blah%'"); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer NOT NULL PRIMARY KEY, value varchar)"), 'created thing table'); my $sth = $dbh->prepare('insert into thing values (?,?)'); ok($sth, 'Prepared insert statement'); $sth->execute(1,'Number One'); $sth->execute(2,'Number Two'); $sth->execute(3,'Number Three'); $sth->execute(4,'Number Four'); $sth->execute(5,'Number Five'); $sth->finish; ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ value => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 11_create_with_delegated_property.t000444023532023421 310512121654175 21727 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More 'no_plan'; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; UR::Object::Type->define( class_name => 'Acme::Manufacturer', is => 'UR::Object', has => [qw/name/], ); UR::Object::Type->define( class_name => 'Acme::Product', has => [ 'name', 'manufacturer' => { is => 'Acme::Manufacturer', id_by => 'manufacturer_id' }, 'genius' ] ); my $m1 = Acme::Manufacturer->create(name => "Lockheed Martin"); my $m2 = Acme::Manufacturer->create(name => "Boeing"); my $m3 = Acme::Manufacturer->create(name => "Explosives R US"); my $p = Acme::Product->create(name => "jet pack", genius => 6, manufacturer => $m1); ok($p, "created a product"); is($p->manufacturer_id,$m1->id,"manufacturer on product is correct"); is($p->manufacturer,$m1,"manufacturer on product is correct"); __END__ Acme::Product->create(name => "hang glider", genius => 4, manufacturer => $m2); Acme::Product->create(name => "mini copter", genius => 5, manufacturer => $m2); Acme::Product->create(name => "firecracker", genius => 6, manufacturer => $m3); Acme::Product->create(name => "dynamite", genius => 7, manufacturer => $m3); Acme::Product->create(name => "plastique", genius => 8, manufacturer => $m3); print Data::Dumper::Dumper(Acme::Product->get(name => 'jet pack')); exit; is(Acme::Product->get(name => "jet pack")->manufacturer->name, "Lockheed Martin"); is(Acme::Product->get(name => "dynamite")->manufacturer->name, "Explosives R US"); 93_namespace.t000444023532023421 404712121654175 15443 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 31; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; is(URT->class, 'URT', 'Namespace name'); my $class_meta = URT->get_member_class('URT::Thingy'); ok($class_meta, 'get_member_class'); is($class_meta->class_name, 'URT::Thingy', 'get_member_class returned the right class'); # This is basically the list of Perl modules under URT/ # note that the 38* classes do not compile because they use data sources that exist # only during that test, and so are not returned by get_material_classes() my @expected_class_names = map { 'URT::' . $_ } qw( 34Baseclass 34Subclass 43Primary 43Related Context::Testing DataSource::CircFk DataSource::Meta DataSource::SomeFile DataSource::SomeFileMux DataSource::SomeMySQL DataSource::SomeOracle DataSource::SomePostgreSQL DataSource::SomeSQLite ObjWithHash RAMThingy Thingy Vocabulary ); my @class_metas = URT->get_material_classes; is(scalar(@class_metas), scalar(@expected_class_names), 'get_material_classes returned expected number of items'); foreach (@class_metas) { isa_ok($_, 'UR::Object::Type'); } my @class_names = sort map { $_->class_name } @class_metas; is_deeply(\@class_names, \@expected_class_names, 'get_material_classes'); my @data_sources = sort URT->get_data_sources; foreach ( @data_sources) { isa_ok($_, 'UR::DataSource'); } my @expected_ds_names = map { 'URT::' . $_ } qw( DataSource::CircFk DataSource::Meta DataSource::SomeFile DataSource::SomeFileMux DataSource::SomeMySQL DataSource::SomeOracle DataSource::SomePostgreSQL DataSource::SomeSQLite ); my @data_source_names = sort map { $_->class } @data_sources; is_deeply(\@data_source_names, \@expected_ds_names, 'get_data_sources'); 34_autouse_with_circular_ur_classdef.t000444023532023421 210112121654175 22445 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; plan skip_all => "known broken - if a parent class has a property of a type which is a subclass of itself, the subclass must explicitly 'use' its parent instead of relying on autoloading"; #plan tests => 2; # KNOWN SOLUTION: # class definition happens in two phases: the minimal phase, then building the detailed meta objects # when one definition triggers loading of other classes, the minimal phase should complete for everything, then the final should run on everythign # this is much like we do when bootstrapping, and if it were in place special boostrapping logic might not be needed # until then: if you do a bunch of cirucular crap with your classes explicitly have them "use" each other :) # make sure things being associated with objects # are not being copied in the constructor use_ok("URT::34Subclass"); my $st = URT::34Subclass->create(); ok($st,"made subclass"); 87e_missing_hangoff_data_is_efficient.t000444023532023421 1702412121654175 22537 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 44; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # Tests a class that has optional hangoff data. # query for objects, including hints for the hangoffs, and then call the # accessor for the hangoff data. The accessors should not trigger additional # DB queries, even for those with missing hangoff data. my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar )'), 'created person table'); ok($dbh->do('create table PERSON_INFO (pi_id int NOT NULL PRIMARY KEY, person_id integer REFERENCES person(person_id), key varchar, value_class_name varchar, value_id varchar)'), 'created person_info table'); $dbh->do("insert into person values (1,'Kermit')"); $dbh->do("insert into person_info values (1,1,'color', 'UR::Value::Text', 'green')"); $dbh->do("insert into person_info values (2,1,'species', 'UR::Value::Text','frog')"); $dbh->do("insert into person_info values (3,1,'food', 'UR::Value::Text','flies')"); $dbh->do("insert into person values (2,'Miss Piggy')"); $dbh->do("insert into person_info values (4,2,'color','UR::Value::Text','pink')"); $dbh->do("insert into person_info values (5,2,'species','UR::Value::Text','pig')"); $dbh->do("insert into person_info values (6,2,'sport','UR::Value::Text','karate')"); $dbh->do("insert into person_info values (7,2,'truelove','URT::Person','1')"); $dbh->do("insert into person values (3,'Fozzy')"); $dbh->do("insert into person_info values (8,3,'color','UR::Value::Text','brown')"); $dbh->do("insert into person_info values (9,3,'species','UR::Value::Text','bear')"); $dbh->do("insert into person_info values (10,3,'sport','UR::Value::Text','golf')"); ok(UR::Object::Type->define( class_name => 'URT::Person', data_source => 'URT::DataSource::SomeSQLite', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, infos => { is => 'URT::PersonInfo', reverse_as => 'person', is_many => 1 }, color => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'color'] }, species => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'species'] }, food => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'food'], is_optional => 1 }, sport => { is => 'Text', via => 'infos', to => 'value_id', where => [key => 'sport'], is_optional => 1 }, truelove => { is => 'URT::Person', via => 'infos', to => 'value_obj', where => [key => 'truelove'], is_optional => 1 }, ], ), 'Created class for main'); ok(UR::Object::Type->define( class_name => 'URT::PersonInfo', table_name => 'PERSON_INFO', data_source => 'URT::DataSource::SomeSQLite', id_by => [ pi_id => { is => 'Number' }, ], has => [ person => { is => 'URT::Person', id_by => 'person_id' }, key => { is => 'Text' }, value_class_name => { is => 'Text' }, value_id => { is => 'Text' }, value_obj => { is => 'UR::Object', id_class_by => 'value_class_name', id_by => 'value_id' }, ], ), "Created class for person_info"); my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); my $thing; $query_count = 0; my $kermit = URT::Person->get(id => 1, -hints => ['color','species','food','sport','truelove']); ok($kermit, 'Got person 1'); is($query_count, 1, 'made 1 query'); $query_count = 0; is($kermit->name, 'Kermit', 'Name is Kermit'); is($query_count, 0, 'Made no queries for direct property'); $query_count = 0; is($kermit->color, 'green', 'Color is green'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->species, 'frog', 'species is frog'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->food, 'flies', 'food is fies'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->sport, undef, 'sport is undef'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($kermit->truelove, undef, 'truelove is undef'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; my $piggy = URT::Person->get(id => 2, -hints => ['color','sport']); ok($piggy, 'Got person 2'); is($query_count, 1, 'made 1 query'); $query_count = 0; is($piggy->name, 'Miss Piggy', 'Name is Miss Piggy'); is($query_count, 0, 'Made no queries for direct property'); $query_count = 0; is($piggy->color, 'pink', 'Color is pink'); is($query_count, 0, 'Made no queries for indirect, hinted property'); $query_count = 0; is($piggy->species, 'pig', 'species is pig'); is($query_count, 1, 'Made one query for indirect, non-hinted property'); $query_count = 0; is($piggy->food, undef, 'food is undef'); is($query_count, 1, 'Made one query for indirect, non-hinted property'); $query_count = 0; is($piggy->sport, 'karate', 'sport is karate'); is($query_count, 0, 'Made no queries for indirect, hinted property'); #$query_count = 0; #is($piggy->truelove, $kermit, 'truelove is kermit!'); #is($query_count, 0, 'Made no queries for indirect, hinted property'); sub unload_everything { for my $o (URT::PersonInfo->is_loaded()) { $o->unload } for my $o (URT::Person->is_loaded()) { $o->unload } my @loaded = URT::PersonInfo->is_loaded(); is(scalar(@loaded), 0, "no hangoff data loaded"); } my (@muppets, @loaded); unload_everything(); $query_count = 0; @muppets = URT::Person->get('truelove.id' => 1); is(scalar(@muppets), 1, "got one muppet that loves kermit"); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only loaded the object needed and the comparison object, and not the other object in the table (successfully wrote the where clause)"); unload_everything(); $kermit = URT::Person->get(1); $query_count = 0; @muppets = URT::Person->get('truelove' => $kermit); is(scalar(@muppets), 1, "got one muppet that loves kermit") or diag(\@muppets); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only found the new object and the parameter object in the cachee (succesffully wrote the where clause to exclude the other db data)"); unload_everything(); $kermit = URT::Person->get(1); $query_count = 0; @muppets = URT::Person->get('truelove.food' => 'flies'); is(scalar(@muppets), 1, "got one muppet that loves someone who eats flies") or diag(\@muppets); is($query_count, 1, "only did one query to get the muppet: succesfully re-wrote the join chain through a generic UR::Object to one with a data source and beyond"); @loaded = URT::Person->is_loaded(); is(scalar(@loaded), 2, "only found the new object and the parameter object in the cachee (succesffully wrote the where clause to exclude the other db data)"); 63c_view_with_subviews.t.expected.cat_set.json000444023532023421 23712121654175 23751 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t{ "id" : "Acme::Cat/And/owner_id/O:\u001dO:111\u001e", "members" : [ { "id" : "222" }, { "id" : "333" } ] } 32_ur_object_id.t000444023532023421 1230112121654175 16140 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More tests => 38; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; use Data::UUID; my $tc1 = class TestClass1 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], }; my $tc2 = class TestClass2 { id_by => ['foo','bar'], has => [ foo => { is => 'String' }, bar => { is => 'String' }, value => { is => 'String' }, ], }; my $tc3 = class TestClass3 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], id_generator => '-uuid', }; my $class_tc4_generator = 0; my $tc4 = class TestClass4 { id_by => 'foo', has => [ foo => { is => 'String' }, value => { is => 'String' }, ], id_generator => sub { ++$class_tc4_generator }, }; my $o; $o = TestClass1->create(foo => 'aaaa', value => '1234'); ok($o, "Created TestClass1 object with explicit ID"); is($o->foo, 'aaaa', "Object's explicit ID has the correct value"); is($o->foo, $o->id, "Object's implicit ID property is equal to the explicit property's value"); $o = TestClass1->create(value => '2345'); ok($o, "Created another TestClass1 object with an autogenerated ID"); ok($o->foo, "The object has an autogenerated ID"); is($o->foo, $o->id, "The object's implicit ID property is equal to the explicit property's value"); my @id_parts = split(' ',$o->id); is($id_parts[0], Sys::Hostname::hostname(), 'hostname part of ID seen'); is($id_parts[1], $$, 'process ID part of ID seen'); # the 2nd part is the time and not reliably checked is($id_parts[3], $UR::Object::Type::autogenerate_id_iter, 'Iterator number part of ID seen'); TestClass1->dump_error_messages(0); TestClass1->queue_error_messages(1); my $error_messages = TestClass1->error_messages_arrayref(); $o = TestClass1->create(foo => 'aaaa', value => '123456'); ok(!$o, "Correctly couldn't create an object with a duplicated ID"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error message'); like($error_messages->[0], qr/An object of class TestClass1 already exists with id value 'aaaa'/, 'The error message was correct'); $o = TestClass2->create(foo => 'aaaa', bar => 'bbbb', value => '1'); ok($o, "Created a TestClass2 object with both explicit ID properties"); is($o->foo, 'aaaa', "First explicit ID property has the right value"); is($o->bar, 'bbbb', "Second explicit ID property has the right value"); is($o->id, join("\t",'aaaa','bbbb'), "Implicit ID property has the right value"); TestClass2->dump_error_messages(0); TestClass2->queue_error_messages(1); $error_messages = TestClass2->error_messages_arrayref(); $o = TestClass2->create(foo => 'qqqq', value => 'blah'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: bar/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(bar => 'wwww', value => 'blah'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs, again"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: foo/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(value => 'asdf'); ok(!$o, "Correctly couldn't create a multi-ID property object without specifying all the IDs, again"); is(scalar(@$error_messages), 1, 'Correctly trapped 1 error messages'); like($error_messages->[0], qr/Attempt to create TestClass2 with multiple ids without these properties: foo, bar/, 'The error message was correct'); @$error_messages = (); $o = TestClass2->create(foo => 'aaaa', bar => 'bbbb', value => '2'); ok(!$o, "Correctly couldn't create another object with duplicated ID properites"); like($error_messages->[0], qr/An object of class TestClass2 already exists with id value 'aaaa\tbbbb'/, 'The error message was correct'); $o = TestClass3->create(foo => 'aaaa', value => '1234'); ok($o, "Created TestClass3 object with explicit ID"); is($o->foo, 'aaaa', "Object's explicit ID has the correct value"); is($o->foo, $o->id, "Object's implicit ID property is equal to the explicit property's value"); my $ug = eval { Data::UUID->new->from_hexstring('0x' . $o->foo) }; ok(((! $ug) or ($ug eq pack('x16'))), 'It was not a properly formatted UUID'); $o = TestClass3->create(value => '2345'); ok($o, "Created another TestClass3 object with an autogenerated ID"); ok($o->foo, "The object has an autogenerated ID"); is($o->foo, $o->id, "The object's implicit ID property is equal to the explicit property's value"); $ug = Data::UUID->new->from_hexstring($o->foo); ok($ug, 'It was a properly formatted UUID'); $o = TestClass4->create(value => '12344'); ok($o, 'Created TestClass4 object with an autogenerated ID'); is($class_tc4_generator, 1, 'The generator anonymous sub was called'); is($o->id, $class_tc4_generator, 'The object ID is as expected'); 95b_subclass_description_preprocessor_errors.t000444023532023421 147412121654175 24300 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use above 'UR'; use Test::More; class Base { is => 'UR::Object', subclass_description_preprocessor => 'Base::_preprocess', subclassify_by => 'subclass_name', }; package Base; sub _preprocess { my ($class, $desc) = @_; my $count_prop = $desc->{has}{count}; $desc->{has}{extra_property} = { is => 'Number', data_type => 'Number', property_name => 'extra_property', class_name => $count_prop->{class_name}, }; return $desc; } package main; eval { class Derived { is => 'Base', has => [ count => { is => 'Number', }, ], }; }; ok($@, "specifying redundant/ambiguous properties via preprocessing is an error"); done_testing(); 42_rpc_between_processes.t000444023532023421 1725312121654175 20107 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; BEGIN { use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; } use URT; use above "UR"; use Test::More;# skip_all => "fork() causes intermittent failure in TAP output"; use Test::Fork; use IO::Socket; # TCP sockets are used when running separate processes for # debugging the test # Let the system pick a socket for us, and then close it. We'll use ReuseAddr # when we re-open it our $PORT; { my $s = IO::Socket::INET->new(Listen => 1, Proto => 'tcp'); $PORT = $s->sockport(); } STDOUT->autoflush(1); STDERR->autoflush(1); my($to_server,$to_client); if ($ARGV[0] and $ARGV[0] ne '--parent' and $ARGV[0] ne '--child') { ($to_server, $to_client) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC); } elsif ($ARGV[0] and $ARGV[0] eq '--child') { # This is for debugging the test case. # It will start up just the child part note('Starting up the child portion'); &Child(); exit(0); } plan tests => 35; my $pid; if ($ARGV[0] and $ARGV[0] eq '--parent') { 1; # do nothing special #} elsif (! ($pid = fork())) { # # child - server # &Child(); # exit(0); #} } else { $pid = fork_ok(6, \&Child); } END { unless ($ARGV[0]) { if ($pid) { note("killing child PID $pid\n"); kill 'TERM', $pid; } elsif (getppid() != 1) { note("Child is exiting early... killing parent"); kill 'TERM', getppid(); } } } # parent #plan tests => 28; unless ($to_server) { sleep(1); # Give the child a change to get started $to_server = IO::Socket::INET->new(PeerHost => '127.0.0.1', PeerPort => $PORT); } $to_client && $to_client->close(); ok($to_server, 'Created a socket connected to the child process ' . $!); my @join_args = ('one','two','three','four'); my $msg = UR::Service::RPC::Message->create( #target_class => 'URT::RPC::Thingy', method_name => 'join', params => ['-', @join_args], 'wantarray' => 0, ); ok($msg, 'Created an RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); my $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my $expected_return_value = join('-',@join_args); my @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single return value'); is($return_values[0], $expected_return_value, 'Response return value is correct'); is($resp->exception, undef, 'Response correctly has no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'illegal', params => \@join_args, 'wantarray' => 0, ); ok($msg, 'Created another RPC message'); ok($msg->send($to_server), 'Sent RPC message from client'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); is($resp->exception, 'Not allowed', 'Response excpetion is correctly set'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'some_undefined_function', params => [], 'wantarray' => 0, ); ok($msg, 'Created third RPC message encoding an undefined function call'); ok($msg->send($to_server), 'Sent RPC message from client'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 0, 'Response return value is correctly empty'); ok($resp->exception =~ m/(Can't locate object method|Undefined sub).*some_undefined_function/, 'Response excpetion correctly reflects calling an undefined function'); my $string = 'a string with some words'; my $pattern = '(\w+) (\w+) (\w+)'; my $regex = qr($pattern); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 0, ); ok($msg, 'Created RPC message for match in scalar context'); ok($msg->send($to_server), 'Sent RPC message to server'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); @return_values = $resp->return_value_list; is(scalar(@return_values), 1, 'Response had a single value'); is($return_values[0], 1, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); $msg = UR::Service::RPC::Message->create( target_class => 'URT::RPC::Thingy', method_name => 'match', params => [$string, $regex], 'wantarray' => 1, ); ok($msg, 'Created RPC message for match in list context'); ok($msg->send($to_server), 'Sent RPC message to server'); $resp = UR::Service::RPC::Message->recv($to_server,1); ok($resp, 'Got a response message back from the server'); my @expected_return_value = qw(a string with); is_deeply($resp->return_value_arrayref, \@expected_return_value, 'Response had the correct return value'); is($resp->exception, undef, 'There was no exception'); sub Child { #plan tests => 6; ok(UR::Object::Type->define( class_name => 'URT::RPC::Listener', is => 'UR::Service::RPC::TcpConnectionListener'), 'Created class for RPC socket Listener'); ok(UR::Object::Type->define( class_name => 'URT::RPC::Thingy', is => 'UR::Service::RPC::Executer'), 'Created class for RPC executor'); unless ($to_client) { $to_client = IO::Socket::INET->new(LocalPort => $PORT, Proto => 'tcp', Listen => 5, Reuse => 1); } $to_server && $to_server->close(); ok($to_client, 'Created TCP listen socket'); my $listen_executer = URT::RPC::Listener->create(fh => $to_client); ok($listen_executer, 'Created RPC executer for the listen socket'); my $rpc_server = UR::Service::RPC::Server->create(); ok($rpc_server, 'Created an RPC server'); ok($rpc_server->add_executer($listen_executer), 'Added the listen executer to the server'); #$rpc_server->add_executer($listen_executer); note('Child process entering the event loop'); while(1) { $rpc_server->loop(undef); } } # END of the main script package URT::RPC::Listener; sub worker_class_name { 'URT::RPC::Thingy'; } package URT::RPC::Thingy; sub authenticate { my($self,$msg) = @_; if ($msg->method_name eq 'illegal') { #$URT::RPC::Thingy::exception++; $msg->exception('Not allowed'); return; } else { return 1; } } sub join { my($joiner,@args) = @_; #$URT::RPC::Thingy::join_called++; my $string = join($joiner, @args); return $string; } # A thing that will return different values in scalar and list context sub match { my($string, $regex) = @_; # my $pattern = qr($pattern); return $string =~ $regex; } 25_recurse_get.t000444023532023421 736312121654175 16015 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 41; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; # Make a tree structure of data: # A # B C # D E # # Another node Z that's not connected to the other tree my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table node ( node_id varchar not null primary key, parent_node_id varchar )'), 'created node table'); my $sth = $dbh->prepare('insert into node values (?,?)'); foreach my $data ( ['A', undef], ['B', 'A'], ['C', 'A'], ['D', 'B'], ['E', 'B'], ['Z', undef ] ) { ok($sth->execute(@$data), 'Insert a row'); } UR::Object::Type->define( class_name => 'URT::Node', id_by => 'node_id', has => [ parent_node_id => { is => 'Text' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'node', ); my @n; foreach ( 0 .. 1 ) { # first time through, no objects are loaded so it'll hit the DB # second time, everything should be in the object cache with results handled # by Indexes # Retrieve the tree rooted at B @n = URT::Node->get(id => 'B', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 3, 'Three nodes rooted at B'); is_deeply([ sort map { $_->id } @n], ['B','D','E'], 'Nodes were correct'); # Retrieve the tree rooted at A @n = URT::Node->get(id => 'A', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 5, 'Five nodes rooted at A'); is_deeply([ sort map { $_->id } @n], ['A','B','C','D','E'], 'Nodes were correct'); # Retrieve the tree rooted at Z @n = URT::Node->get(id => 'Z', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 1, 'One node rooted at Z'); is_deeply([ sort map { $_->id } @n], ['Z'], 'Nodes were correct'); # Retrieve the tree rooted at Q @n = URT::Node->get(id => 'Q', -recurse => [ parent_node_id => 'node_id' ] ); is(scalar(@n), 0, 'No nodes with id Q'); } for ( 0 .. 1 ) { # first time through, unload everything. # second time, everything should be in the object cache with results handled # by Indexes if (! $_) { ok(URT::Node->unload(), 'Unload all URT::Node objects'); } # Retrieve the path from E to the root @n = URT::Node->get(id => 'E', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 3, 'Three nodes from E to the root'); is_deeply([ sort map { $_->id } @n], ['A','B','E'], 'Nodes were correct'); # Retrieve the path from C to the root @n = URT::Node->get(id => 'C', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 2, 'Three nodes from C to the root'); is_deeply([ sort map { $_->id } @n], ['A','C'], 'Nodes were correct'); # Retrieve the path from A to the root @n = URT::Node->get(id => 'A', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 1, 'One node from A to the root'); is_deeply([ sort map { $_->id } @n], ['A'], 'Nodes were correct'); # Retrieve the path from Z to the root @n = URT::Node->get(id => 'Z', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 1, 'One node from Z to the root'); is_deeply([ sort map { $_->id } @n], ['Z'], 'Nodes were correct'); # Retrieve the path from Q to the root @n = URT::Node->get(id => 'Q', -recurse => [node_id => 'parent_node_id'] ); is(scalar(@n), 0, 'No nodes from Q to the root'); } 15_singleton.t000444023532023421 474512121654175 15510 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 23; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; my $co = UR::Object::Type->define( class_name => 'URT::Parent', ); ok($co, 'Defined a parent, non-singleton class'); $co = UR::Object::Type->define( class_name => 'URT::SomeSingleton', is => ['URT::Parent','UR::Singleton'], has => [ property_a => { is => 'String' }, ], ); ok($co, 'Defined URT::SomeSingleton class'); $co = UR::Object::Type->define( class_name => 'URT::ChildSingleton', is => [ 'URT::SomeSingleton','UR::Singleton' ], has => [ property_b => { is => 'String' }, ], ); ok($co, 'Defined URT::ChildSingleton class'); $co = UR::Object::Type->define( class_name => 'URT::GrandChild', is => [ 'URT::ChildSingleton'], ); ok($co, 'Defined URT::GrandChild class'); ok(URT::GrandChild->create(id => 'URT::GrandChild', property_a => 'foo', property_b=>'bar'), 'Created a URT::GrandChild object'); my $obj = URT::SomeSingleton->_singleton_object(); ok($obj, 'Got the URT::SomeSingleton object through _singleton_object()'); isa_ok($obj, 'URT::SomeSingleton'); is($obj->property_a('hello'), 'hello', 'Setting property_a on URT::SomeSingleton object'); is($obj->property_a(), 'hello', 'Getting property_a on URT::SomeSingleton object'); my $obj2 = URT::SomeSingleton->get(); ok($obj2, 'Calling get() on URT::SomeSingleton returns an object'); is_deeply($obj,$obj2, 'The two objects are the same'); $obj = URT::ChildSingleton->_singleton_object(); ok($obj, 'Got the URT::ChildSingleton object through _singleton_object()'); isa_ok($obj, 'URT::ChildSingleton'); isa_ok($obj, 'URT::SomeSingleton'); is($obj->property_a('foo'), 'foo', 'Setting property_a on URT::ChildSingleton object'); is($obj->property_a(), 'foo', 'Getting property_a on URT::ChildSingleton object'); is($obj->property_b('blah'), 'blah', 'Setting property_b on URT::ChildSingleton object'); is($obj->property_b(), 'blah', 'Getting property_b on URT::ChildSingleton object'); $obj2 = URT::ChildSingleton->get(); ok($obj2, 'Calling get() on URT::ChildSingleton returns an object'); is_deeply($obj,$obj2, 'The two objects are the same'); my @objs = URT::Parent->get(); is(scalar(@objs), 3, 'get() via parent class returns 3 objects'); ok($obj->delete(), 'Delete the URT::ChildSingleton'); @objs = URT::Parent->get(); is(scalar(@objs), 2, 'get() via parent class returns 2 objects'); 17_accessor_object_basic.t000444023532023421 545112121654175 17774 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 31; UR::Object::Type->define( class_name => 'Acme', is => ['UR::Namespace'], ); UR::Object::Type->define( class_name => "Acme::Boss", has => [ id => { type => "Number" }, name => { type => "String" }, ] ); UR::Object::Type->define( class_name => 'Acme::Employee', has => [ name => { type => "String" }, #boss => { type => "Acme::Boss", id_by => [ boss_id => { type => "Number" } ] }, boss => { type => "Acme::Boss", id_by => 'boss_id' }, ] ); my $c = Acme::Employee->__meta__; my @p = sort $c->all_property_names; is_deeply(\@p, [qw/boss_id name/], "got expected old-style properties"); ok(Acme::Employee->can("boss_id"), "has an accessor for the fk property."); ok(Acme::Employee->can("boss"), "has an accessor for the object."); my $b1 = Acme::Boss->create(name => "Bossy", id => 1000); ok($b1, "made a boss"); my $b2 = Acme::Boss->create(name => "Crabby", id => 2000); ok($b2, "made another boss"); ok($b1 != $b2, "boss objects are different"); ok($b1->id != $b2->id, "boss ids are different"); my $e = Acme::Employee->create(name => "Shifty", id => 3000, boss_id => $b1->id); ok($e, "made an employee"); is($e->boss_id,$b1->id, "the boss is assigned correctly when using the id at creation time and getting the id"); is($e->boss,$b1, "the boss is assigned correctly when using the id at creation time and getting the object"); is($e->boss($b2),$b2, "assigned a different boss object"); is($e->boss_id, $b2->id, "boss id is okay"); is($e->boss, $b2, "boss object is okay"); is($e->boss(undef), undef, "Set the boss to undef"); is($e->boss_id, undef, "No boss_id on the new employee"); is($e->boss, undef, "No boss on the new employee"); is($e->boss($b1), $b1, "Set the boss back to a real object"); is($e->boss,$b1, "the boss is object is back"); is($e->boss_id, $b1->id, "boss id is back too"); is($e->boss_id(undef), undef, "Set the id to undef"); is($e->boss_id, undef, "No boss_id on the new employee"); is($e->boss, undef, "No boss on the new employee"); my $e2 = Acme::Employee->create(name => "Nappy"); ok($e2, "Made a new employee"); is($e2->boss_id, undef, "No boss_id on the new employee"); is($e2->boss, undef, "No boss on the new employee"); is($e->boss($b1), $b1, "set one boss to one object"); is($e2->boss($b2), $b2, "set another boss to the other object"); ok($e->boss != $e2->boss, "boss objects differ as expected"); my $e3 = Acme::Employee->create(name => "Snappy", boss => $b1); ok($e3, "Made a new employee with a boss property"); is($e3->boss, $b1, "No boss on the new employee"); is($e3->boss_id, $b1->id, "No boss_id on the new employee"); 04d_oracle.t000444023532023421 65212121654175 15066 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More skip_all => "enable after configuring Oracle"; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace my $dbh = URT::DataSource::SomeOracle->get_default_handle; ok($dbh, "got a handle"); isa_ok($dbh, 'UR::DBI::db', 'Returned handle is the proper class'); 1; 24_query_via_method_call.t000444023532023421 244612121654175 20041 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table product ( product_id int NOT NULL PRIMARY KEY, product_name varchar, product_type varchar)'), 'created product table'); ok($dbh->do("insert into product values (1,'race car', 'cool')"), 'insert row into product for race car'); ok($dbh->do("insert into product values (2,'pencil','notcool')"), 'insert row into product for pencil'); sub URT::Product::me { my $self = shift; return $self; } UR::Object::Type->define( class_name => 'URT::Product', id_by => 'product_id', has => [ product_name => { is => 'Text' }, product_type => { is => 'Text' }, #me_name => { via => 'me', to => 'product_name' }, me_name => { via => '__self__', to => 'product_name' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'product', ); my @p = URT::Product->get(me_name => 'race car'); is(scalar(@p), 1, 'Got one product that is_cool'); is($p[0]->product_name, 'race car', 'name is correct'); 35_all_objects_are_loaded_subclass.t000444023532023421 1106412121654175 22037 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 21; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); our $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); our $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(); is(scalar(@o), 2, 'URT::Parent->get returned 2 parent objects'); is($load_count, 2, 'loaded 2 Parent objects'); is($query_count, 2, 'get() triggered 2 queries'); # 1 on the parent table, 1 more for child joined to parent $load_count = 0; $query_count = 0; @o = URT::Child->get(); is(scalar(@o), 1, 'URT::Child->get returned 1 child object'); is($load_count, 0, 'correctly loaded 0 objects - gotten from the cache'); is($query_count, 0, 'get() correctly triggered 0 queries'); $load_count = 0; $query_count = 0; @o = URT::OtherChild->get(); is(scalar(@o), 0, 'URT::OtherChild->get returned 0 other child objects'); is($load_count, 0, 'loaded 0 times - all from the cache'); # Note that the original parent get() would have triggered a query joining other_child table # to parent if there were any other_child objects is($query_count, 0, 'get() correctly triggered 0 query'); unlink(URT::DataSource::SomeSQLite->server); # Remove the file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, the_type_name varchar)'), 'created parent table'); ok($dbh->do('create table CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), child_value varchar )'), 'created child table'); ok($dbh->do('create table OTHER_CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), other_child_value varchar )'), 'created other child table'); #@URT::Parent::ISA = ('UR::ModuleBase'); #@URT::Child::ISA = ('UR::ModuleBase'); #@URT::OtherChild::ISA = ('UR::ModuleBase'); #ok(UR::Object::Type->define( # class_name => 'URT', # is => 'UR::Namespace', # ), # "Created namespace for URT"); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'the_type_name' => { is => 'STRING'}, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', table_name => 'CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ child_value => { is => 'STRING' }, ], ), "Created class for Child" ); ok(UR::Object::Type->define( class_name => 'URT::OtherChild', table_name => 'OTHER_CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ other_child_value => { is => 'STRING' }, ], ), "Created class for Other Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values (1, 'Bob', 'URT::Parent'))), "insert a parent object"); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values ( 2, 'Fred', 'URT::Child'))), "Insert part 1 of a child object"); ok($dbh->do(q(insert into child_table (child_id, child_value) values ( 2, 'stuff'))), "Insert part 2 of a child object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return $obj->the_type_name; } 85_avoid_loading_using_hints.t000444023532023421 1166612121654175 20746 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 18; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; # Test getting some objects that includes -hints, and then that later get()s # don't re-query the DB use URT; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got a database handle'); ok($dbh->do('create table PERSON ( person_id int NOT NULL PRIMARY KEY, name varchar, is_cool integer )'), 'created person table'); ok($dbh->do('create table CAR ( car_id int NOT NULL PRIMARY KEY, color varchar, is_primary int, owner_id integer references PERSON(person_id))'), 'created car table'); ok($dbh->do('create table car_parts ( part_id int NOT NULL PRIMARY KEY, name varchar, price integer, car_id integer references CAR(car_id))'), 'created car_parts table'); ok(UR::Object::Type->define( class_name => 'URT::Person', table_name => 'PERSON', id_by => [ person_id => { is => 'NUMBER' }, ], has => [ name => { is => 'String' }, is_cool => { is => 'Boolean' }, cars => { is => 'URT::Car', reverse_as => 'owner', is_many => 1, is_optional => 1 }, primary_car => { is => 'URT::Car', via => 'cars', to => '__self__', where => ['is_primary true' => 1] }, primary_car_parts => { via => 'primary_car', to => 'parts' }, car_color => { via => 'cars', to => 'color' }, car_parts => { via => 'cars', to => 'parts', is_optional => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), 'Created class for people'); ok(UR::Object::Type->define( class_name => 'URT::Car', table_name => 'CAR', id_by => [ car_id => { is => 'NUMBER' }, ], has => [ color => { is => 'String' }, is_primary => { is => 'Boolean' }, owner => { is => 'URT::Person', id_by => 'owner_id' }, parts => { is => 'URT::CarParts', reverse_as => 'car', is_many => 1 }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for Car"); ok(UR::Object::Type->define( class_name => 'URT::CarParts', table_name => 'CAR_PARTS', id_by => 'part_id', has => [ name => { is => 'String' }, price => { is => 'Integer' }, car => { is => 'URT::Car', id_by => 'car_id' }, ], data_source => 'URT::DataSource::SomeSQLite', ), "Created class for CarParts"); # Insert some data # Bob and Mike have red cars, Fred and Joe have blue cars. Frank has no car. Bob, Joe and Frank are cool # Bob also has a yellow car that's his primary car my $insert = $dbh->prepare('insert into person values (?,?,?)'); foreach my $row ( [ 1, 'Bob',1 ], [2, 'Fred',0], [3, 'Mike',0],[4,'Joe',1], [5,'Frank', 1] ) { $insert->execute(@$row); } $insert->finish(); $insert = $dbh->prepare('insert into car values (?,?,?,?)'); foreach my $row ( [ 1,'red',0, 1], [ 2,'blue',1, 2], [3,'red',1,3],[4,'blue',1,4],[5,'yellow',1,1] ) { $insert->execute(@$row); } $insert->finish(); # Bob's non-primary car has wheels and engine, # Bob's primary car has custom wheels and neon lights # Fred's car has wheels and seats # Mike's car has engine and radio # Joe's car has seats and radio $insert = $dbh->prepare('insert into car_parts values (?,?,?,?)'); foreach my $row ( [1, 'wheels', 100, 1], [2, 'engine', 200, 1], [3, 'wheels', 100, 2], [4, 'seats', 50, 2], [5, 'engine', 200, 3], [6, 'radio', 50, 3], [7, 'seats', 50, 4], [8, 'radio', 50, 4], [9, 'custom wheels', 200, 5], [10,'neon lights', 100, 5], ) { $insert->execute(@$row); } my $query_count = 0; my $query_text = ''; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_text = $_[0]; $query_count++}), 'Created a subscription for query'); $query_count = 0; my @people = URT::Person->get(is_cool => 1, -hints => ['car_parts']); is(scalar(@people), 3, '3 people are cool'); is($query_count, 1, 'Made 1 query'); $query_count = 0; my @car = $people[0]->cars; is(scalar(@car), 2, 'Got car objects from first person through accessor'); is($query_count, 0, 'Made no queries'); $query_count = 0; @car = URT::Car->get(owner_id => $people[0]->id); is(scalar(@car),2 , 'Got car objects from first person from URT::Car class'); is($query_count, 0, 'Made no queries'); $query_count = 0; @people = URT::Person->get(is_cool => 1); is(scalar(@people), 3, '3 people are cool (no hints)'); is($query_count, 0, 'Made no queries'); $query_count = 0; my @parts = $people[0]->primary_car->parts; is(scalar(@parts), 2, "First person's car has 2 parts"); is($query_count, 0, 'Made no queries'); 49f_complicated_get_indirect_id_by.t000444023532023421 1117512121654175 22050 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # This tests a get() where the filtering property has several levels of indirection # - A Person has a Job, which has a Location, which has a phone number # - A Person's phone number for their job can be linked together a couple # of different ways # 1) # a) Location has a phone number # b) Job has-a location, id-by the location_id property # c) Job has-a phone number via the Location # d) Person has-a Job, linked with the job_id property # e) Person has-a job_phone, via the Job (which is via the Location) # 2) a,b same as above # c) Person has-a location_id via the Job # d) Person has-a Location, id-by the location_id (which is via the Job) # e) Person has-a work_phone, via the Location &setup_classes_and_db(); # This is the way we usually do a doubly-indirect property my $person = URT::Person->get(job_phone => '456-789-0123'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Joe', 'Got the right person'); is($person->job_name, 'cleaner', 'With the right job name'); is ($person->job_phone, '456-789-0123', 'the right job_phone'); is ($person->work_phone, '456-789-0123', 'and the right work_phone'); # This one wasn't working before I fixed UR::Object::Property::_get_joins() $person = URT::Person->get(work_phone => '123-456-7890'); ok($person, 'get() returned an object'); isa_ok($person, 'URT::Person'); is($person->name, 'Bob', 'Got the right person'); is($person->job_name, 'cook', 'With the right job name'); is($person->job_phone, '123-456-7890', 'the right job_phone'); is($person->work_phone, '123-456-7890', 'and the right work_phone'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table locations (location_id integer, phone_number varchar, address varchar)"), 'Created locations table'); ok( $dbh->do("create table jobs (job_id integer, job_name varchar, location_id integer REFERENCES locations(location_id))"), 'Created jobs table'); ok( $dbh->do("create table persons (person_id integer, name varchar, job_id integer REFERENCES jobs(job_id))"), 'Created persons table'); # First person $dbh->do("insert into locations (location_id, phone_number, address) values (1,'123-456-7890','123 Fake St')"); $dbh->do("insert into jobs (job_id, job_name, location_id) values (1, 'cook', 1)"); $dbh->do("insert into persons (person_id, name, job_id) values(1,'Bob', 1)"); # second $dbh->do("insert into locations (location_id, phone_number, address) values (2,'456-789-0123','987 Main St')"); $dbh->do("insert into jobs (job_id, job_name, location_id) values (2, 'cleaner', 2)"); $dbh->do("insert into persons (person_id, name, job_id) values(2,'Joe', 2)"); ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::Location', id_by => [ location_id => { is => 'Integer' }, ], has => [ phone_number => { is => 'String' }, address => { is => 'String' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'locations', ); UR::Object::Type->define( class_name => 'URT::Job', id_by => [ job_id => { is => 'Integer' }, ], has => [ job_name => { is => 'String' }, location => { is => 'URT::Location', id_by => 'location_id' }, location_phone => { via => 'location', to => 'phone_number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'jobs', ); UR::Object::Type->define( class_name => 'URT::Person', id_by => [ person_id => { is => 'Integer' }, ], has => [ name => { is => 'String' }, job_id => { is => 'Integer' }, job => { is => 'URT::Job', id_by => 'job_id' }, job_name => { via => 'job' }, job_phone => { via => 'job', to => 'location_phone' }, work_location_id => { via => 'job', to => 'location_id' }, work_location => { is => 'URT::Location', id_by => 'work_location_id' }, work_phone => { via => 'work_location', to => 'phone_number' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'persons', ); } 70_command_help_text.t000444023532023421 635712121654175 17202 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse warnings; use strict; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Test::More tests => 15; BEGIN { $ENV{'ANSI_COLORS_DISABLED'} = 1 } UR::Object::Type->define( class_name => 'Acme::ParentCommand', is => 'Command', has => [ param_a => { is => 'String', is_optional => 1, doc => 'Some documentation for param a' }, param_b => { is => 'String', is_optional => 0, example_values => ['1','2','3'] }, param_c => { is => 'String', doc => 'Parent documentation for param c' }, ], ); UR::Object::Type->define( class_name => 'Acme::ChildCommand', is => 'Acme::ParentCommand', has => [ param_a => { is => 'String', is_optional => 0 }, param_c => { is => 'String', doc => 'Child documentation for param c' }, ], ); sub Acme::ParentCommand::execute { 1; } sub Acme::ChildCommand::execute { 1; } my $usage_string = ''; my $callback = sub { my $self = shift; $usage_string = shift; $usage_string =~ s/\x{1b}\[\dm//g; # Remove ANSI escape sequences for color/underline }; Acme::ParentCommand->dump_usage_messages(0); Acme::ParentCommand->usage_messages_callback($callback); Acme::ChildCommand->dump_usage_messages(0); Acme::ChildCommand->usage_messages_callback($callback); $usage_string = ''; my $rv = Acme::ParentCommand->_execute_with_shell_params_and_return_exit_code('--help'); is($rv, 0, 'Parent command executed'); like($usage_string, qr(USAGE\s+acme parent-command --param-b=\?\s+--param-c=\?\s+\[--param-a=\?\]), 'Parent help text usage is correct'); like($usage_string, qr(REQUIRED ARGUMENTS\s+param-b\s+String), 'Parent help text lists param-b as required'); like($usage_string, qr(OPTIONAL ARGUMENTS\s+param-a\s+String\s+Some documentation for param a), 'Parent help text lists param-a as optional'); like($usage_string, qr(param-c\s+String\s+Parent documentation for param c), 'Parent help text for param c'); unlike($usage_string, qr(REQUIRED ARGUMENTS\s+param-a\s+String), 'Parent help text does not list param-a as required'); unlike($usage_string, qr(OPTIONAL ARGUMENTS\s+param-b\s+String), 'Parent help text does not list param-b as optional'); $usage_string = ''; $rv = Acme::ChildCommand->_execute_with_shell_params_and_return_exit_code('--help'); is($rv, 0, 'Child command executed'); like($usage_string, qr(USAGE\s+acme child-command --param-a=\?\s+--param-b=\?\s+--param-c=\?), 'Child help text usage is correct'); like($usage_string, qr(param-a\s+String\s+Some documentation for param a), 'Child help text mentions param-a with parent documentation'); like($usage_string, qr(param-b\s+String), 'Child help text mentions param-b'); like($usage_string, qr(param-c\s+String\s+Child documentation for param c), 'Child help text mentions param-c with child documentation'); unlike($usage_string, qr(OPTIONAL ARGUMENTS\s+param-a\s+String), 'Child help text does not list param-a as optional'); my $meta = Acme::ParentCommand->__meta__; my $p_meta_b = $meta->property('param_b'); my $example_values_arrayref = $p_meta_b->example_values; is("@$example_values_arrayref", "1 2 3", "example values are stored"); is(scalar(@$example_values_arrayref), 3, "example value count is as expected"); 23_id_class_by_accessor.t000444023532023421 542712121654175 17640 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use UR; use Test::More tests => 11; # This test is to reproduce a poor error message that was received # when trying to access an indirect object which contained an invalid # class name. In the previous case this simply died to trying to call # __meta__ on an undefined value within the accessor sub of # mk_id_based_object_accessor. class TestClass { has => [ other_class => { is => 'Text' }, other_id => { is => 'Number' }, other => { is => 'UR::Object', id_class_by => 'other_class', id_by => 'other_id'}, ], }; class RelatedThing { id_by => 'id', has => [ name => { is => 'String' } ], }; my $a = TestClass->create(other_class => 'NonExistent', other_id => '1234'); my $other = eval { $a->other }; ok(! $other, 'Calling id_class_by accessor with bad data threw exception'); like($@, qr(Can't resolve value for 'other' on class TestClass id), 'Exception looks ok'); my $related = RelatedThing->create(name => 'bob'); my $b = TestClass->create(other_class => 'RelatedThing', other_id => $related->id); ok($b, 'Created thing'); is($b->other->id, $related->id, "Thing's other accessor returne the previously created object"); # Wheels are attached to things. # Clocks have wheels. class Clock { has_many => [ wheels => { is => 'Wheel', reverse_as => 'attached_to' } ], }; class Wheel { has => [ attached_to => { is => 'UR::Object', id_class_by => 'attached_to_class', id_by => 'attached_to_id' } ], }; my $clock = Clock->create(); my $clock_wheel0 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my $clock_wheel1 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my $clock_wheel2 = Wheel->create(attached_to_class => 'Clock', attached_to_id => $clock->id); my @clock_wheels = $clock->wheels(); is(scalar(@clock_wheels), 3, 'Clock has 3 wheels'); is($clock_wheels[0]->id, $clock_wheel0->id, 'Wheel 0 has correct ID'); is($clock_wheels[1]->id, $clock_wheel1->id, 'Wheel 1 has correct ID'); is($clock_wheels[2]->id, $clock_wheel2->id, 'Wheel 2 has correct ID'); # Vehicles also have wheels. Motorcycles are vehicles. class Vehicle { is_abstract => 1, has_many => [ wheels => { is => 'Wheel', reverse_as => 'attached_to' } ], }; class Motorcycle { is => 'Vehicle' }; my $moto = Motorcycle->create(); my $moto_wheel0 = Wheel->create(attached_to_class => 'Motorcycle', attached_to_id => $moto->id); my $moto_wheel1 = Wheel->create(attached_to_class => 'Motorcycle', attached_to_id => $moto->id); my @moto_wheels = $moto->wheels(); is(scalar(@moto_wheels), 2, 'Motorcycle has 2 wheels'); is($moto_wheels[0]->id, $moto_wheel0->id, 'Wheel 0 has correct ID'); is($moto_wheels[1]->id, $moto_wheel1->id, 'Wheel 1 has correct ID'); 44_modulewriter.t000444023532023421 1132612121654175 16243 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse Test::More; use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; plan tests => 5; # First, make a couple of classes we can point to my $c = UR::Object::Type->define( class_name => 'URT::Related', id_by => [ related_id => { is => 'String' }, related_id2 => { is => 'String' }, ], has => [ related_value => { is => 'String'}, ], ); ok($c, 'Defined URT::Related class'); $c = UR::Object::Type->define( class_name => 'URT::Parent', id_by => [ parent_id => { is => 'String' }, ], has => [ parent_value => { is => 'String' }, ], ); ok($c, 'Defined URT::Parent class'); $c = UR::Object::Type->define( class_name => 'URT::Remote', id_by => [ remote_id => { is => 'Integer' }, ], has => [ # test_obj => { is => 'URT::TestClass', id_by => ['prop1','prop2','prop3'] }, something => { is => 'String' }, ], ); ok($c, 'Defined URT::Remote class'); # Make up a class definition with all the different kinds of properties we can think of... # FIXME - I'm not sure how the attributes_have and id_implied stuff is meant to work my $test_class_definition = q( is => [ 'URT::Parent' ], table_name => 'PARENT_TABLE', attributes_have => [ meta_prop_a => { is => 'Boolean', is_optional => 1 }, meta_prop_b => { is => 'String' }, ], subclassify_by => 'my_subclass_name', id_by => [ another_id => { is => 'String', doc => 'blahblah' }, related => { is => 'URT::Related', id_by => [ 'parent_id', 'related_id' ], doc => 'related' }, foobaz => { is => 'Integer' }, ], has => [ property_a => { is => 'String', meta_prop_a => 1 }, property_b => { is => 'Integer', is_abstract => 1, meta_prop_b => 'metafoo', doc => 'property_b' }, calc_sql => { calculate_sql => q(to_upper(property_b)) }, some_enum => { is => 'Integer', column_name => 'SOME_ENUM', valid_values => [100,200,300] }, another_enum => { is => 'String', column_name => 'different_name', valid_values => ["one","two","three",3,"four"] }, my_subclass_name => { is => 'Text', calculate_from => [ 'property_a', 'property_b' ], calculate => q("URT::TestClass") }, subclass_by_prop => { is => 'String', implied_by => 'subclass_by_obj' }, subclass_by_id => { is => 'Integer', implied_by => 'subclass_by_obj' }, subclass_by_obj => { is => 'UR::Object', id_by => 'subclass_by_id', id_class_by => 'subclass_by_prop' }, ], has_many => [ property_cs => { is => 'String', is_optional => 1 }, remotes => { is => 'URT::Remote', reverse_as => 'testobj', where => [ something => { operator => 'like', value => '%match%' } ] }, ], has_optional => [ property_d => { is => 'Number' }, calc_perl => { calculate_from => [ 'property_a', 'property_b' ], calculate => q($property_a . $property_b) }, another_related => { is => 'URT::Related', id_by => [ 'rel_id1', 'rel_id2' ], where => [ property_a => 'foo' ] }, related_value => { is => 'StringSubclass', via => 'another_related' }, related_value2 => { is => 'StringSubclass', via => 'another_related', to => 'related_value', is_mutable => 1 }, ], schema_name => 'SomeFile', data_source => 'URT::DataSource::SomeFile', id_generator => 'the_sequence_seq', valid_signals => ['nonstandard1', 'something_else', 'third_thing'], doc => 'Hi there', ); my $orig_test_class = $test_class_definition; my $test_class_meta = eval "UR::Object::Type->define(class_name => 'URT::TestClass', $test_class_definition);"; ok($test_class_meta, 'Defined URT::TestClass class'); if ($@) { diag("Errors from class definition:\n$@"); exit(1); } my $string = $test_class_meta->resolve_class_description_perl(); my $orig_string = $string; # Normalize them by removing newlines, and multiple spaces $test_class_definition =~ s/\n//gm; $test_class_definition =~ s/\s+/ /gm; $string =~ s/\n//gm; $string =~ s/\s+/ /gm; my $diffcmd = 'sdiff -s '; if ($string ne $test_class_definition) { ok(0, 'Rewritten class definition matches original'); #is($string, $test_class_definition, 'Rewritten class definition matches original'); diag("Original definition string:\n$orig_test_class\n"); diag("Generated definition:\n$orig_string\n"); IO::File->new('>/tmp/old')->print($orig_test_class); IO::File->new('>/tmp/new')->print($orig_string); system "$diffcmd/tmp/old /tmp/new"; } else { ok(1, 'Rewritten class definition matches original'); } 75_custom_loader.t000444023532023421 272512121654175 16350 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 3; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; ### package URT::SelfLoader1; use URT; class URT::SelfLoader1 { has => [qw/nose tail/], data_source => 'UR::DataSource::Default', }; sub __load__ { my ($class, $bx, $headers) = @_; # for testing purposes we ignore the $bx and $headers, # and return a 2-row, 3-column data set $headers = ['nose','tail','id']; my $body = [ ['wet','waggly', 1001], ['dry','perky', 1002], ]; my $iterator = sub { shift @$body }; return $headers, $iterator; } ### package main; my $new = URT::SelfLoader1->create(nose => 'long', tail => 'floppy', id => 1003); ok($new, "made a new object"); # The system will trust the db engine, but then will merge results with any objects # already in memory. This means our new object matches, and even though only one # of the database rows match, the broken db above will return 2 more items. Totalling 3. my @p1 = URT::SelfLoader1->get(nose => ['long','wet']); is(scalar(@p1), 2, "got two objects as expected, because we re-check the query engine by default"); # Now that the query results are cached, the bug in the db logic is hidden, and we return # the full results. my @p2 = URT::SelfLoader1->get(nose => ['long','wet']); is(scalar(@p2), 2, "got two objects as expected"); ### 1; 53_abandoned_iterator.t000444023532023421 703412121654175 17326 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 15; use URT::DataSource::SomeSQLite; &setup_classes_and_db(); my $iter = URT::Thing->create_iterator(thing_value => { operator => '<', value => 15}); my @objects; for (my $i = 1; $i < 10; $i++) { push @objects, $iter->next(); } is(scalar(@objects), 9, 'Loaded 9 objects through the (still open) iterator'); my @objects2 = URT::Thing->get(thing_value => { operator => '<', value => 15 } ); is(scalar(@objects2), 14, 'get() with same params loads all relevant objects from the DB'); $iter = undef; @objects2 = URT::Thing->get(thing_value => { operator => '<', value => 15 } ); is(scalar(@objects2), 14, 'get() with same params loads all relevant objects from the DB after undeffing the iterator'); URT::Thing->unload(); $iter = undef; $iter = URT::Thing->create_iterator(); ok($iter, 'Created iterator with no filters'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; # Now try to get all the objects @objects2 = URT::Thing->get(); is(scalar(@objects2), 19, 'get() with no filters returns all the objects after undefining the iterator'); URT::Thing->unload(); $iter = URT::Thing->create_iterator(thing_value => { operator => 'like', value => '%1%' }); ok($iter, 'Created iterator with filter on thing_value'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; @objects2 = URT::Thing->get(thing_value => { operator => 'like', value => '%1%' }); is(scalar(@objects2), 11, 'get() with the same filter on thing_value returns all the objects'); URT::Thing->unload(); $iter = URT::Thing->create_iterator(thing_one => 1); ok($iter, 'Created iterator with filter on thing_one'); @objects = (); for ( my $i = 0; $i < 9; $i++) { my $o = $iter->next(); unless ($o) { ok(0, 'calling next() on the iterator did not return an object'); } push @objects, $o; } is(scalar(@objects), 9, 'Loaded only the first 9 objects from the iterator'); $iter = undef; @objects2 = URT::Thing->get(thing_one => 1); is(scalar(@objects2), 19, 'get() with the same filter on thing_one returns all the objects'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing (thing_id integer, thing_value integer, thing_one integer)"), 'Created thing table'); my $insert = $dbh->prepare("insert into thing (thing_id, thing_value, thing_one) values (?,?,1)"); for (my $i = 1; $i < 20; $i++) { unless($insert->execute($i,$i)) { ok(0, 'Failed in insert test data to DB'); exit; } } $insert->finish; ok(1, 'Inserted test data to DB'); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ thing_value => { is => 'Integer' }, thing_one => { is => 'Integer' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); } 09_create_get_complex2.t000444023532023421 342212121654175 17413 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 5; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my ($p1,$p2,$p3,$p4,$p5,$p6,$p7,@obj); use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name genius/] ); $p1 = Acme::Product->create(name => "jet pack", genius => 6, manufacturer_name => "Lockheed Martin"); $p2 = Acme::Product->create(name => "hang glider", genius => 4, manufacturer_name => "Boeing"); $p3 = Acme::Product->create(name => "mini copter", genius => 5, manufacturer_name => "Boeing"); $p4 = Acme::Product->create(name => "firecracker", genius => 6, manufacturer_name => "Explosives R US"); $p5 = Acme::Product->create(name => "dynamite", genius => 7, manufacturer_name => "Explosives R US"); $p6 = Acme::Product->create(name => "plastique", genius => 8, manufacturer_name => "Explosives R US"); $p7 = Acme::Product->create(name => "mega copter", genius => 2, manufacturer_name => "Cheap Chopper"); @obj = Acme::Product->get(name => { operator => "like", value => '%copter' }); is(scalar(@obj),2, 'Two objects match name like "%copter"'); @obj = Acme::Product->get(genius => { operator => ">=", value => 6 }); is(scalar(@obj),4, '4 objects have genius >= 6'); @obj = Acme::Product->get(genius => { operator => "between", value => [5,7] }); is(scalar(@obj),4, '4 objects have genius between 5 and 7'); @obj = Acme::Product->get('genius between' => [5,7] ); is(scalar(@obj),4, '4 objects have genius between 5 and 7 (alternate syntax)'); @obj = sort Acme::Product->get(name => { operator => "not in", value => ['jet pack', 'dynamite'] }); is(scalar(@obj),5, '5 objects have name not in ["jet pack","dynamite"]'); 03i_non_ur_types_as_values.t000444023532023421 526412121654175 20437 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests=> 55; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; use URT; use IO::Handle; use IO::File; # A non-UR Perl class package OtherScalar; our @ISA = (); package SomeScalar; our @ISA = qw(OtherScalar); package main; ok(UR::Object::Type->define( class_name => 'URT::Person', id_by => [ person_id => { is => 'Number' }, ], has => [ name => { is => 'Text' }, list_thing => { is => 'ARRAY' }, glob_thing => { is => 'GLOB' }, handle_thing => { is => 'IO::Handle' }, scalar_thing => { is => 'SCALAR' }, code_thing => { is => 'CODE' }, hash_thing => { is => 'HASH' }, ref_thing => { is => 'REF' }, ], ), "created class for Person"); my $listref = [1,2,3]; my $blessed_listref = [1,2,3]; bless $blessed_listref,'ListRef'; my $handle = IO::Handle->new(); my $filehandle = IO::File->new(); our $FOO; my $globref = \*FOO; my $scalarref = \"hello"; my $blessed_scalarref; { my $string = "hello"; $blessed_scalarref = \$string; bless $blessed_scalarref, 'ScalarRef'; } my $other_scalarref; # In package SomeScalar, which ISA OtherScalar { my $string = "hello"; $other_scalarref = \$string; bless $other_scalarref, 'SomeScalar'; } my $coderef = sub {1;}; my $blessed_coderef = sub {1;}; bless $blessed_coderef, 'CodeRef'; my $hashref = { one => 1, two => 2 }; my $blessed_hashref = { one => 1, two => 2 }; bless $blessed_hashref, 'HashRef'; my $refref = \$listref; my $blessed_refref = \$listref; bless $blessed_refref, 'RefRef'; my @tests = ( [ name => 'Bob' ], [ list_thing => $listref ], [ glob_thing => $handle ], [ glob_thing => $filehandle ], [ glob_thing => $globref ], [ handle_thing => $handle ], [ handle_thing => $filehandle ], [ scalar_thing => $scalarref ], [ scalar_thing => $blessed_scalarref ], [ scalar_thing => $other_scalarref ], [ scalar_thing => 1 ], [ code_thing => $coderef ], [ code_thing => $blessed_coderef ], [ hash_thing => $hashref ], [ hash_thing => $blessed_hashref ], [ ref_thing => $refref ], [ ref_thing => $blessed_refref ], [ ref_thing => $hashref ], ); foreach my $test ( @tests ) { my($bx,%extra) = URT::Person->define_boolexpr( @$test ); ok($bx, 'Created BoolExpr with params '.join(',',@$test)); is($bx->value_for($test->[0]), $test->[1], 'Value for param is correct'); is(scalar(keys %extra), 0, 'No params were rejected by define_boolexpr()'); } 08_create_get_complex1.t000444023532023421 266412121654175 17420 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use Test::More tests => 3; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__).'/../..'; my @obj; use UR; UR::Object::Type->define( class_name => 'Acme::Product', has => [qw/name manufacturer_name genius/] ); Acme::Product->create(name => "jet pack", genius => 6, manufacturer_name => "Lockheed Martin"); Acme::Product->create(name => "hang glider", genius => 4, manufacturer_name => "Boeing"); Acme::Product->create(name => "mini copter", genius => 5, manufacturer_name => "Boeing"); Acme::Product->create(name => "catapult", genius => 5, manufacturer_name => "Boeing"); Acme::Product->create(name => "firecracker", genius => 6, manufacturer_name => "Explosives R US"); Acme::Product->create(name => "dynamite", genius => 9, manufacturer_name => "Explosives R US"); Acme::Product->create(name => "plastique", genius => 8, manufacturer_name => "Explosives R US"); @obj = Acme::Product->get(manufacturer_name => 'Boeing', genius => 5); is(scalar(@obj),2, "Two objects match manufacturer_name => 'Boeing', genius => 5"); @obj = Acme::Product->get(name => ['jet pack', 'dynamite']); is(scalar(@obj),2, 'Two object match name "jet pack" or "dynamite"'); @obj = Acme::Product->get(manufacturer_name => ['Boeing','Lockheed Martin']); is(scalar(@obj),4, '4 objects have manufacturer_name Boeing or Lockheed Martin'); 12_properties_metadata_query.t000444023532023421 311612121654175 20753 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use strict; use warnings; plan tests => 7; class X { has => [ x1 => { is => 'Text', doc => 'this is property x1 boo' }, x2 => { is => 'Text', doc => 'this is property x2' }, x3 => { is => 'Text', doc => 'this is property x3' }, x4 => { is => 'Text', doc => 'this is property x4' }, ], }; class Y { is => ['X'], has => [ y1 => { is => 'Text', doc => 'this is property y blah1' }, y2 => { is => 'Text', doc => 'this is property y2 boo' }, x1 => { doc => 'override of x1 in Y' }, x4 => { doc => 'override of x4 in Y' }, ], }; class Z { is => ['Y'], has => [ z1 => { is => 'Text', doc => 'this is property z1' }, z2 => { is => 'Text', doc => 'this is property z2 blah' }, y1 => { doc => 'override of y1 in Z' }, x3 => { doc => 'override of x1 in Z' }, x4 => { doc => 'override of x4 in Z which is also overriden in Y' }, ], }; my $m = Z->__meta__; ok($m, "got meta for class Z"); my @p; my $p; @p = $m->_properties(); is(scalar(@p), 9, "got 8 properties, as expected"); @p = $m->_properties("doc like" => '%x4%'); is(scalar(@p), 1, "got 1 x4 property"); $p = $p[0]; is($p->class_name, "Z", "class name is Z as expected"); is($p->property_name, "x4", "property name is x4 as expected"); $p = $m->property('x1'); ok($p, "got 1 x1 property"); is($p->property_name,"x1","property name is correct"); 03a_rules.t000444023532023421 1602112121654175 15004 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 45; use Data::Dumper; class URT::Item { id_by => [qw/name group/], has => [ name => { is => "String" }, group => { is => "String" }, parent => { is => "URT::Item", is_optional => 1, id_by => ['parent_name','parent_group'] }, foo => { is => "String", is_optional => 1 }, bar => { is => "String", is_optional => 1 }, score => { is => 'Integer' }, ] }; class URT::FancyItem { is => 'URT::Item', has => [ feet => { is => "String" } ] }; class URT::UnrelatedItem { has => [ name => { is => "String" }, group => { is => "String" }, nicknames => { is_many => 1, is => "Integer" }, ], }; my $m = URT::FancyItem->__meta__; ok($m, "got metadata for test class"); my @p = $m->id_property_names; is("@p", "name group", "property names are correct"); my $b = URT::Item->create(name => 'Joe', group => 'shirts'); ok($b, 'made a base class object'); my $p = URT::FancyItem->create(name => 'Bob', group => 'shirts', score => 1, foo => 'foo'); ok($p, "made a parent object"); my $c = URT::FancyItem->create(parent => $p, name => 'Fred', group => 'skins', score => 2); ok($c, "made a child object which references it"); my $u = URT::UnrelatedItem->create(name => 'Bob', group => 'shirts'); ok($u, 'made an unrelated item object'); my $bx1 = URT::Item->define_boolexpr(name => ['Bob','Joe']); my @o = URT::Item->get($bx1); is(scalar(@o), 2, "got 2 items with an in-clause"); ## OR ## my $bx2a = URT::Item->define_boolexpr(name => 'Bob'); my $bx2b = URT::Item->define_boolexpr(group => 'skins'); my $bx2t = UR::BoolExpr::Template::Or->get_by_subject_class_name_logic_type_and_logic_detail( $bx2a->subject_class_name, 'Or', $bx2a->logic_detail . '|' . $bx2b->logic_detail, ); my $bx2c = $bx2t->get_rule_for_values('Bob','skins'); ok(defined($bx2c), "got OR rule: $bx2c"); my ($bx3a,$bx3b) = $bx2c->template->get_underlying_rule_templates(); is($bx3a,$bx2a->template, "first expression in composite matches"); is($bx3b,$bx2b->template, "second expression in composite matches"); my $bx3 = URT::Item->define_boolexpr(-or => [[name => 'Bob'], [group => 'skins']]); ok(defined($bx3), "created OR rule in a single expression"); is_deeply( $bx3, $bx2c, "matches the one individually composed"); my %as_two = map { $_->id => $_ } (URT::Item->get($bx2a), URT::Item->get($bx2b)); my %as_one = map { $_->id => $_ } URT::Item->get($bx3); my @as_two = sort keys %as_two; my @as_one = sort keys %as_one; is("@as_one","@as_two", "results using -or match queries done separately"); # COMPLEX #my $r = URT::FancyItem->define_boolexpr(foo => 222, -recurse => [qw/parent_name name parent_group group/], bar => 555); my $r = URT::Item->define_boolexpr(foo => ''); # '' is the same as undef ok($r, "Created a rule to get URT::Items with null 'foo's"); ok($r->specifies_value_for('foo'), 'Rule specifies a falue for foo'); is($r->value_for('foo'), '', "rule's value for property foo is empty string"); ok(! $r->specifies_value_for('name'), 'rule does not specify a value for name'); my @results = URT::Item->get($r); is(scalar(@results), 2, 'Got 2 URT::Items with the rule'); ok(scalar(grep { $_->name eq 'Joe' } @results), 'Joe was returned'); ok(scalar(grep { $_->name eq 'Fred' } @results), 'Fred was returned'); ok(! scalar(grep { $_->name eq 'Bob' } @results), 'Bob was not returned'); $r = URT::FancyItem->define_boolexpr(foo => 222, -recurse => [parent_name => 'name', parent_group => 'group'], bar => 555); ok($r, "got a rule to get objects using -recurse"); is($r->template->value_position_for_property_name('foo'),0, "position is as expected for variable param 1"); is($r->template->value_position_for_property_name('bar'),1, "position is as expected for variable param 2"); is($r->template->value_position_for_property_name('-recurse'),0, "position is as expected for constant param 1"); my $expected = [foo => 222, -recurse => [qw/parent_name name parent_group group/], bar => 555]; is_deeply( [$r->params_list], $expected, "params list for the rule is as expected" ) or print Dumper([$r->params_list],$expected); my $t = $r->template; ok($t, "got a template for the rule"); is($t->value_position_for_property_name('foo'),0, "position is as expected for variable param 1"); is($t->value_position_for_property_name('bar'),1, "position is as expected for variable param 2"); is($t->value_position_for_property_name('-recurse'),0, "position is as expected for constant param 1"); my @names = $t->_property_names; is("@names","foo bar", "rule template knows its property names"); my $r2 = $t->get_rule_for_values(333,666); ok($r2, "got a new rule from the template with different values for the non-constant values"); is_deeply( [$r2->params_list], [foo => 333, -recurse => [qw/parent_name name parent_group group/], bar => 666], "the new rule has the expected structure" ) or print Dumper([$r->params_list]); $r = URT::FancyItem->define_boolexpr(foo => { operator => "between", value => [10,30] }, bar => { operator => "like", value => 'x%y' }); $t = $r->template(); is($t->operator_for('foo'),'between', "operator for param 1 is correct"); is($t->operator_for('bar'),'like', "operator for param 2 is correct"); $r = URT::FancyItem->define_boolexpr(foo => 10, bar => { operator => "like", value => 'x%y' }); $t = $r->template(); is($t->operator_for('foo'),'=', "operator for param 1 is correct"); is($t->operator_for('bar'),'like', "operator for param 2 is correct"); $r = URT::FancyItem->define_boolexpr(foo => { operator => "between", value => [10,30] }, bar => 20); $t = $r->template(); is($t->operator_for('foo'),'between', "operator for param 1 is correct"); is($t->operator_for('bar'),'=', "operator for param 2 is correct"); # Make a rule on the parent class $r = URT::Item->define_boolexpr(name => 'Bob', group => 'shirts', score => '01'); ok($r->evaluate($p), 'Original parent object evaluated though rule'); ok(! $r->evaluate($c), 'Child object with different params evaluated through parent rule returns false'); $r = URT::Item->define_boolexpr(name => 'Fred', group => 'skins'); ok($r->evaluate($c), 'Child object with same params evaluated through parent rule returns true'); # Make a rule on the child class $r = URT::FancyItem->define_boolexpr(name => 'Joe', group => 'shirts'); ok(! $r->evaluate($b), 'Base class object evaluated through rule on child class returns false'); # An item of a different class but with the same params $r = URT::UnrelatedItem->define_boolexpr(name => 'Bob', group => 'shirts'); ok(! $r->evaluate($p), 'Original parent object evaluated false through rule on unrelatd class'); my $j = URT::UnrelatedItem->create(name => 'James', group => 'shirts', nicknames => [12345, 12347, 34, 36, 37]); $r = URT::UnrelatedItem->define_boolexpr(nicknames => [12347, 82]); ok($r->evaluate($j), 'Many-to-many comparison finds the matching nickname'); 55_on_the_fly_metadb.t000444023532023421 732012121654175 17144 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use Test::More; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use File::Temp; use Sub::Install; use strict; use warnings; # This test assummes the storage DB schema already exists, but that the metaDB has # no record of it. plan tests => 20; # Make a data source # There is a bug w/ the file temp path generated on Mac OS X and SQLite. # Fix, and restore this to use File::Temp. my $db_file = '/tmp/pid' . $$; END { unlink $db_file; } # Hrm... seems that 'server' still isn't a proper property yet IO::File->new($db_file,'w')->close(); sub URT::DataSource::OnTheFly::server { return $db_file } my $ds = UR::Object::Type->define( class_name => 'URT::DataSource::OnTheFly', is => 'UR::DataSource::SQLite', ); ok($ds, 'Defined data source'); # Connect to the datasource's DB directly, create a couple of tables and some seed data my $dbh = URT::DataSource::OnTheFly->get_default_handle; ok($dbh->do('create table TABLE_A (a_id integer PRIMARY KEY, a_value varchar)'), 'Created TABLE_A'); ok($dbh->do('create table TABLE_B (b_id integer PRIMARY KEY, a_id int references TABLE_A(a_id))'), 'Created TABLE_B'); ok($dbh->do("insert into TABLE_A (a_id, a_value) values (10,'hello')"), 'Inserted row into table_a'); ok($dbh->do("insert into TABLE_B (b_id, a_id) values (2,10)"), 'Inserted row into table_b'); ok($dbh->commit(), 'Inserts committed to the DB'); # Define a couple of classes to go with those tables # Note that we're not going to insert anything in the MetaDB about # these tables my $class_a = UR::Object::Type->define( class_name => 'URT::ClassA', id_by => ['a_id'], has => [ a_value => { is => 'Text' }, ], data_source => $ds->id, table_name => 'TABLE_A' ); ok($class_a, 'Defined ClassA'); my $class_b = UR::Object::Type->define( class_name => 'URT::ClassB', id_by => ['b_id'], has => [ a_obj => { is => 'URT::ClassA', id_by => 'a_id' }, ], data_source => $ds->id, table_name => 'TABLE_B', ); ok($class_b, 'Defined ClassB'); # Now interact with the object API to get/create/save data my @results; @results = URT::ClassA->get(10); ok(scalar(@results) == 1, 'We can get an item from ClassA'); @results = URT::ClassB->get(2); ok(scalar(@results) == 1, 'We can get an item from ClassB'); @results = URT::ClassB->get(1); ok(scalar(@results) == 0, 'Get ClassB with non-existent ID correctly returns 0 items'); my $new_a = URT::ClassA->create(a_value => 'there'); ok($new_a, 'We are able to create a new ClassA item'); my $new_b = URT::ClassB->create(a_id => $new_a->a_id); ok($new_b, 'We are able to create a new ClassB item'); ok(UR::Context->commit(), 'Committed to the DB successfully'); # Check that the data made it to the DB my $sth = $dbh->prepare('select * from table_a order by a_id'); ok($sth, 'select on table_a prepared'); $sth->execute(); my $results = $sth->fetchall_arrayref(); is(scalar(@$results), 2, 'There are 2 rows in table_a'); is_deeply($results, [[$new_a->id, 'there'], [10, 'hello']], 'Data in table_a is correct'); $sth = $dbh->prepare('select * from table_b order by b_id'); ok($sth, 'select on table_b prepared'); $sth->execute(); $results = $sth->fetchall_arrayref(); is(scalar(@$results), 2, 'There are 2 rows in table_b'); is_deeply($results, [[$new_b->b_id, $new_a->a_id],[2,10]], 'Data in table_a is correct'); 64_nullable_foreign_key_handling_on_insert_and_delete.t000444023532023421 2473112121654175 25776 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 81; use URT::DataSource::CircFk; use Data::Dumper; # This test verifies that sql generation is correct for inserts and deletes # on tables with nullable foreign key constraints. For a new object, an # INSERT statement should be returned, with null values in nullable foreign # key columns, and a corresponding UPDATE statement to set foreign key # values after the insert. For object deletion, an UPDATE statement # setting nullable foreign keys to null is expected with the DELETE statement setup_classes_and_db(); my @circular = URT::Circular->get(); my $sqlite_ds = UR::Context->resolve_data_source_for_object($circular[0]); is (scalar @circular, 5, 'got circular objects'); for (@circular){ my $id = $_->id; ok($_->delete, 'deleted object'); my $ghost = URT::Circular::Ghost->get(id=> $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_update_and_delete(@sql), "got separate update and delete statement for deleting circular item w/ nullable foreign key"); } eval{ UR::Context->commit(); }; ok(!$@, "circular deletion committed successfully!"); diag($@) if $@; my @bridges = URT::Bridge->get(); for (@bridges){ my $id = $_->id; ok($_->delete(), 'deleted bridge'); my $ghost = URT::Bridge::Ghost->get(id => $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_delete_only(@sql), "didn't update primary key nullable foreign keys on delete"); } eval{ UR::Context->commit(); }; ok( !$@, 'no commit errors on deleting bridge entries w/ nullable foreign keys primary key' ); diag($@) if $@; my @bridges_check = URT::Bridge->get(); is (scalar @bridges_check, 0, "couldn't retrieve deleted bridges"); my @left = URT::Left->get(id=>[1..5]); my @right = URT::Right->get(); while (my $left = shift @left){ my $right = shift @right; my $bridge = URT::Bridge->create(left_id => $left->id, right_id => $right->id); my @sql = $sqlite_ds->_default_save_sql_for_object($bridge); ok(sql_has_insert_only(@sql), "didn't null insert values for bridge entries nullable, no update statement produced)"); } eval{ UR::Context->commit(); }; ok( !$@, 'no commit errors on recreating bridge entries' ); diag($@) if $@; my @chain = ( URT::Gamma->get(), URT::Beta->get(), URT::Alpha->get()); ok (@chain, 'got objects from alpha, beta, and gamma tables'); is (scalar @chain, 3, 'got expected number of objects'); my $gamma = shift @chain; ok ($gamma->delete, 'deleted_object'); for ("URT::Beta", "URT::Alpha"){ my $obj = shift @chain; my $id = $obj->id; my $class = $_."::Ghost"; ok($obj->delete, 'deleted object'); my $ghost = $class->get(id => $id); my @sql = $sqlite_ds->_default_save_sql_for_object($ghost); ok(sql_has_update_and_delete(@sql), "got separate update and delete statement for deleting bridge items w/ nullable foreign key"); } eval{ UR::Context->commit(); }; ok(!$@, "no error message on commit: $@"); diag($@) if $@; my @chain2 = (URT::Alpha->get(), URT::Beta->get(), URT::Gamma->get()); ok(!@chain2, "couldn't get deleted chain objects!"); my ($new_alpha, $new_beta, $new_gamma); ok($new_alpha = URT::Alpha->create(id => 101, beta_id => 201), 'created new alpha'); my @alpha_sql = $sqlite_ds->_default_save_sql_for_object($new_alpha); ok($new_beta = URT::Beta->create(id => 201, gamma_id => 301), 'created new beta'); my @beta_sql = $sqlite_ds->_default_save_sql_for_object($new_beta); ok($new_gamma = URT::Gamma->create(id => 301, type => 'test2'), 'created new gamma'); for (\@alpha_sql, \@beta_sql){ ok(sql_has_insert_and_update(@$_), 'got seperate insert and update statements for recreating chained objects'); } eval { UR::Context->commit(); }; ok(!$@, "no error message on commit of new alpha,beta,gamma, would fail due to fk constraints if we weren't using sqlite datasource"); diag($@) if $@; my $check_alpha = URT::Alpha->get(id => 101); is ($check_alpha->beta_id, 201, 'initial null value updated correctly for chain object'); my $check_beta = URT::Beta->get(id => 201); is ($check_beta->gamma_id, 301, 'initial null value updated correctly for chain object'); sub sql_has_delete_only{ my @st = @_; return undef if grep {$_->{sql} =~ /update|insert/i} @st; return undef unless grep {$_->{sql} =~/delete/i} @st; return 1; } sub sql_has_insert_only{ my @st = @_; return undef if grep {$_->{sql} =~ /update|delete/i} @st; return undef unless grep {$_->{sql} =~/insert/i} @st; return 1; } sub sql_has_insert_and_update{ my @st = @_; return undef unless grep {$_->{sql} =~ /insert/i} @st; return undef unless grep {$_->{sql} =~ /update/i} @st; return 1; } sub sql_has_update_and_delete{ my @st = @_; return undef unless grep {my $val = $_; $val->{sql} =~ /delete/i} @st; return undef unless grep {my $val = $_; $val->{sql} =~ /update/i} @st; return 1; } sub setup_classes_and_db { my $dbh = URT::DataSource::CircFk->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table circular (id integer primary key, parent_id integer REFERENCES circular(id))"), 'Created circular table'); ok( $dbh->do("create table left (id integer, right_id integer REFERENCES right(id), right_id2 integer REFERENCES right(id), primary key (id, right_id))"), 'Created left table'); ok( $dbh->do("create table right (id integer primary key, left_id integer REFERENCES left(id), left_id2 integer REFERENCES left(id))"), 'Created right table'); ok( $dbh->do("create table alpha (id integer primary key, beta_id integer REFERENCES beta(id))"), 'Created table alpha'); ok( $dbh->do("create table beta (id integer primary key, gamma_id integer REFERENCES gamma(id))"), 'Created table beta'); ok( $dbh->do("create table gamma (id integer primary key, type varchar)"), 'Created table gamma'); ok( $dbh->do("create table bridge (left_id integer REFERENCES left(id), right_id integer REFERENCES right(id), primary key (left_id, right_id))"), 'Created table bridge'); my $ins_circular = $dbh->prepare("insert into circular (id, parent_id) values (?,?)"); foreach my $row ( [1, 5], [2, 1], [3, 2], [4, 3], [5, 4] ) { ok( $ins_circular->execute(@$row), 'Inserted into circular' ); } $ins_circular->finish; my $ins_left = $dbh->prepare("insert into left (id, right_id, right_id2) values (?,?,?)"); my $ins_right = $dbh->prepare("insert into right (id, left_id, left_id2) values (?,?,?)"); foreach my $row ( ( [1,1,2], [2,2,3], [3,3,4], [4,4,5], [5,5,6]) ) { ok( $ins_left->execute(@$row), 'Inserted into left'); ok( $ins_right->execute(@$row), 'Inserted into right'); } my $ins_bridge_left = $dbh->prepare("insert into left(id) values (?)"); $ins_bridge_left->execute(10); my $ins_bridge_right = $dbh->prepare("insert into right(id) values (?)"); my $ins_bridge = $dbh->prepare("insert into bridge(left_id, right_id) values (?, ?)"); for (11..15){ $ins_bridge_right->execute($_); $ins_bridge->execute(10, $_); } $ins_bridge->finish; $ins_bridge_right->finish; $ins_bridge_left->finish; $ins_left->finish; $ins_right->finish; my $ins_alpha = $dbh->prepare("insert into alpha(id, beta_id) values(?,?)"); ok($ins_alpha->execute(100,200), 'inserted into alpha'); $ins_alpha->finish; my $ins_beta = $dbh->prepare("insert into beta(id, gamma_id) values(?,?)"); ok($ins_beta->execute(200, 300), 'inserted into beta'); $ins_beta->finish; my $ins_gamma = $dbh->prepare("insert into gamma(id, type) values(?,?)"); ok($ins_gamma->execute(300, 'test'), 'inserted into gamma'); $ins_gamma->finish; ok($dbh->commit(), 'DB commit'); ok(UR::Object::Type->define( class_name => 'URT::Circular', id_by => [ id => { is => 'Integer' }, ], has_optional => [ parent_id => { is => 'Integer'}, parent => {is => 'URT::Circular', id_by => 'parent_id'} ], data_source => 'URT::DataSource::CircFk', table_name => 'circular', ), 'Defined URT::Circular class'); ok(UR::Object::Type->define( class_name => 'URT::Left', id_by => [ id => { is => 'Integer'} ], has_optional => [ right_id => { is => 'Integer' }, right => { is => 'URT::Right', id_by => 'right_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'left', ), 'Defined URT::Left class'); ok(UR::Object::Type->define( class_name => 'URT::Right', id_by => [ id => { is => 'Integer'} ], has_optional => [ left_id => { is => 'Integer' }, left => { is => 'URT::Left', id_by => 'left_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'right', ), 'Defined URT::Right class'); ok(UR::Object::Type->define( class_name => 'URT::Alpha', id_by => [ id => {is => 'Integer'} ], has_optional => [ beta_id => { is => 'Integer' }, beta => { is => 'URT::Beta', id_by => 'beta_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'alpha', ), 'Defined URT::Alpha class'); ok(UR::Object::Type->define( class_name => 'URT::Beta', id_by => [ id => {is => 'Integer'} ], has_optional => [ gamma_id => { is => 'Integer' }, gamma => { is => 'URT::Gamma', id_by => 'gamma_id'}, ], data_source => 'URT::DataSource::CircFk', table_name => 'beta', ), 'Defined URT::Beta class'); ok(UR::Object::Type->define( class_name => 'URT::Gamma', id_by => [ id => {is => 'Integer'} ], has => [ type => { is => 'Text' }, ], data_source => 'URT::DataSource::CircFk', table_name => 'gamma', ), 'Defined URT::Alpha class'); ok(UR::Object::Type->define( class_name => 'URT::Bridge', id_by => [ left_id => {is => 'Integer'}, right_id => {is => 'Integer'} ], data_source => 'URT::DataSource::CircFk', table_name => 'bridge', ), 'Defined URT::Bridge class'); } 49_complicated_get.t000444023532023421 1256412121654175 16656 0ustar00abrummetgsc000000000000UR-0.41/t/URT/tuse strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; use Test::More tests => 17; use URT::DataSource::SomeSQLite; # This tests a get() with several unusual properties.... # - The subclass we're get()ting has no table of its own; it inherits one from its parent # - The property we're get()ting with isn't a column in its inherited table, it's delegated # - That delegated property is 'via' another subclass with no table of its own # - The delegated property is 'to' another delegated property # # UR::DataSource::RDBMS was modified to properly determine table/column when the subclass # inherits that table/column from a parent. It also needed to traverse delegated properties # to arbitrary depth to know what the final accessor is. &setup_classes_and_db(); my $thing = URT::Thing::Person->get(job => 'cook'); ok($thing, 'get() returned an object'); isa_ok($thing, 'URT::Thing::Person'); is($thing->name, 'Bob', 'The expected object was returned'); is($thing->job, 'cook', 'the delegated property has the expected value'); sub setup_classes_and_db { my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, 'Got DB handle'); ok( $dbh->do("create table thing_type (type_id integer, type_name varchar)"), 'Created type table'); ok( $dbh->do("create table thing (thing_id integer, name varchar, type_id integer REFERENCES thing_type(type_id))"), 'Created thing table'); ok( $dbh->do("create table param (param_id integer, type varchar, value varchar, type_id integer REFERENCES thing_type(type_id))"), 'Created param table'); my $ins_type = $dbh->prepare("insert into thing_type (type_id, type_name) values (?,?)"); foreach my $row ( ( [1, 'person'], [2, 'car'] ) ) { ok( $ins_type->execute(@$row), 'Inserted a type'); } my $ins_thing = $dbh->prepare("insert into thing (thing_id, name, type_id) values (?,?,?)"); foreach my $row ( ( [1, 'Bob',1], [2, 'Christine',2]) ) { ok( $ins_thing->execute(@$row), 'Inserted a thing'); } $ins_thing->finish; my $ins_params = $dbh->prepare("insert into param (param_id, type, value, type_id) values (?,?,?,?)"); foreach my $row ( ( [1, 'alignment', 'good', 1], [2, 'job', 'cook', 1], [3, 'alignment', 'evil', 2], [4, 'color', 'red', 2] ) ) { ok($ins_params->execute(@$row), 'Inserted a param'); } ok($dbh->commit(), 'DB commit'); UR::Object::Type->define( class_name => 'URT::ThingType', id_by => [ type_id => { is => 'Integer' }, ], has => [ type_name => { is => 'String' }, params => { is => 'URT::Param', reverse_as => 'type_obj', is_many => 1 }, alignment => { via => 'params', to => 'value', where => [param_type => 'alignment'] }, ], is_abstract => 1, sub_classification_method_name => '_type_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing_type', ); UR::Object::Type->define( class_name => 'URT::ThingType::Person', is => 'URT::ThingType', has => [ job => { via => 'params', to => 'value', where => [type => 'job'] }, ] ); UR::Object::Type->define( class_name => 'URT::ThingType::Car', is => 'URT::ThingType', has => [ color => { via => 'params', to => 'value', where => [type => 'color'] }, ] ); UR::Object::Type->define( class_name => 'URT::Thing', id_by => 'thing_id', has => [ name => { is => 'String' }, type_obj => { is => 'URT::ThingType', id_by => 'type_id' }, type => { via => 'type_obj', to => 'type_name' }, params => { via => 'type_obj' }, alignment => { via => 'params' }, ], is_abstract => 1, #sub_classification_property_name => 'type', sub_classification_method_name => '_thing_resolve_subclass_name', data_source => 'URT::DataSource::SomeSQLite', table_name => 'thing', ); UR::Object::Type->define( class_name => 'URT::Thing::Person', is => 'URT::Thing', has => [ type_obj => { is => 'URT::ThingType::Person', id_by => 'type_id' }, job => { via => 'type_obj' }, ], ); UR::Object::Type->define( class_name => 'URT::Thing::Car', is => 'URT::Thing', has => [ type_obj => { is => 'URT::ThingType::Car', id_by => 'type_id' }, color => { via => 'type_obj' }, ], ); UR::Object::Type->define( class_name => 'URT::Param', id_by => 'param_id', has => [ type => { is => 'String' }, value => { is => 'String' }, type_obj => { is => 'URT::ThingType', id_by => 'type_id' }, ], data_source => 'URT::DataSource::SomeSQLite', table_name => 'param', ); } sub URT::Thing::_thing_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type); } sub URT::ThingType::_type_resolve_subclass_name { my($class,$obj) = @_; return $class . '::' . ucfirst($obj->type_name); } 36_superclass_already_loaded.t000444023532023421 1127712121654175 20724 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 22; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use URT; # dummy namespace # Turn this on for debugging #$ENV{UR_DBI_MONITOR_SQL}=1; my $dbh = URT::DataSource::SomeSQLite->get_default_handle; ok($dbh, "got a db handle"); &create_db_tables($dbh); our $load_count = 0; ok(URT::Parent->create_subscription( method => 'load', callback => sub {$load_count++}), 'Created a subscription for load'); our $query_count = 0; ok(URT::DataSource::SomeSQLite->create_subscription( method => 'query', callback => sub {$query_count++}), 'Created a subscription for query'); $load_count = 0; $query_count = 0; my @o = URT::Parent->get(the_type_name => 'URT::Child'); is(scalar(@o), 1, 'URT::Parent->get returned 1 object'); is($load_count, 1, 'loaded 1 objects'); ok($o[0]->isa('URT::Child'), 'Loaded object is of the correct type'); is($query_count, 2, 'get() triggered 2 queries'); # 1 on the parent table, 1 more for child joined to parent $load_count = 0; $query_count = 0; @o = URT::Child->get(the_type_name => 'URT::Child', child_value => 'stuff'); is(scalar(@o), 1, 'URT::Child->get returned 1 child object'); is($load_count, 0, 'currectly loaded 0 objects - gotten from the cache'); is($query_count, 0, 'get() correctly triggered 0 queries'); $load_count = 0; $query_count = 0; @o = URT::OtherChild->get(); is(scalar(@o), 0, 'URT::OtherChild->get returned 0 other child objects'); is($load_count, 0, 'loaded 0 times - all from the cache'); # Note that the original parent get() would have triggered a query joining other_child table # to parent if there were any other_child objects is($query_count, 1, 'get() correctly triggered 1 query'); unlink(URT::DataSource::SomeSQLite->server); # Remove the DB file from /tmp/ sub create_db_tables { my $dbh = shift; ok($dbh->do('create table PARENT_TABLE ( parent_id int NOT NULL PRIMARY KEY, name varchar, the_type_name varchar)'), 'created parent table'); ok($dbh->do('create table CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), child_value varchar )'), 'created child table'); ok($dbh->do('create table OTHER_CHILD_TABLE ( child_id int NOT NULL PRIMARY KEY CONSTRAINT child_parent_fk REFERENCES parent_table(parent_id), other_child_value varchar )'), 'created other child table'); #@URT::Parent::ISA = ('UR::ModuleBase'); #@URT::Child::ISA = ('UR::ModuleBase'); #@URT::OtherChild::ISA = ('UR::ModuleBase'); #ok(UR::Object::Type->define( # class_name => 'URT', # is => 'UR::Namespace', # ), # "Created namespace for URT"); ok(UR::Object::Type->define( class_name => 'URT::Parent', table_name => 'PARENT_TABLE', id_by => [ 'parent_id' => { is => 'NUMBER' }, ], has => [ 'name' => { is => 'STRING' }, 'the_type_name' => { is => 'STRING'}, ], data_source => 'URT::DataSource::SomeSQLite', sub_classification_method_name => 'reclassify_object', ), "Created class for Parent"); ok(UR::Object::Type->define( class_name => 'URT::Child', table_name => 'CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ child_value => { is => 'STRING' }, ], ), "Created class for Child" ); ok(UR::Object::Type->define( class_name => 'URT::OtherChild', table_name => 'OTHER_CHILD_TABLE', is => [ 'URT::Parent' ], id_by => [ child_id => { is => 'NUMBER' }, ], has => [ other_child_value => { is => 'STRING' }, ], ), "Created class for Other Child" ); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values (1, 'Bob', 'URT::Parent'))), "insert a parent object"); ok($dbh->do(q(insert into parent_table (parent_id, name, the_type_name) values ( 2, 'Fred', 'URT::Child'))), "Insert part 1 of a child object"); ok($dbh->do(q(insert into child_table (child_id, child_value) values ( 2, 'stuff'))), "Insert part 2 of a child object"); } sub URT::Parent::reclassify_object { my($class,$obj) = @_; return $obj->the_type_name; } file_datasource000755023532023421 012121654174 15775 5ustar00abrummetgsc000000000000UR-0.41/t/URT/tread_linenum_as_column.t000444023532023421 447612121654172 23032 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 29; use IO::File; use File::Temp; # First write some easy data my $fh = File::Temp->new(); foreach ( 'a','b','c','d','e' ) { $fh->print($_,"\n"); } $fh->close(); my $filename = $fh->filename; ok(UR::Object::Type->define( class_name => 'URT::Alphabet', id_by => [ file => { is => 'String', column_name => '__FILE__'}, lineno => { is => 'Integer', column_name => '$.' }, ], has => [ letter => { is => 'String' }, ], data_source => { is => 'UR::DataSource::Filesystem', path => '$file', columns => ['letter'], }, ), 'Defined class for letters'); my @objs = URT::Alphabet->get(file => $filename, 'lineno <' => 4); is(scalar(@objs), 3, 'Got 3 objects back filtering by lineno < 4'); # because line numbers ($.) start at 1 my @expected = ( { file => $filename, lineno => 1, letter => 'a' }, { file => $filename, lineno => 2, letter => 'b' }, { file => $filename, lineno => 3, letter => 'c' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } @objs = URT::Alphabet->get(file => $filename, lineno => 4); is(scalar(@objs), 1, 'Got 1 object with lineno == 4'); _compare_to_expected($objs[0], { file => $filename, lineno => 4, letter => 'd' }); @objs = URT::Alphabet->get(file => $filename, lineno => 10); is(scalar(@objs), 0, 'Correctly got 0 objects with lineno == 10'); @objs = URT::Alphabet->get(file => $filename, 'lineno between' => [2,7]); is(scalar(@objs), 4, 'Got 4 objects with lineno between 2 and 7'); @expected = ( { file => $filename, lineno => 2, letter => 'b' }, { file => $filename, lineno => 3, letter => 'c' }, { file => $filename, lineno => 4, letter => 'd' }, { file => $filename, lineno => 5, letter => 'e' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } sub _compare_to_expected { my($obj,$expected) = @_; foreach my $prop ( 'file','lineno','letter' ) { is($obj->$prop, $expected->{$prop}, "$prop has expected value"); } } write.t000444023532023421 457512121654173 17463 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 15; use IO::File; use File::Temp; # File data: id name score my @data = ( [1, 'AAA', 1], [2, 'BBB', 1], [4, 'DDD', 1], [5, 'EEE', 1], [6, 'fff', 0], [7, 'ggg', 0], [9, 'iii', 0], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); $datafile->print(join("\t",@$_),"\n") foreach (@data); $datafile->flush(); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", columns => ['letter_id','name','score'], sorted_columns => ['name','letter_id'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Letter', id_by => [ letter_id => { is => 'Number' } ], has => [ name => { is => 'String' }, score => { is => 'Number' }, ], data_source_id => $data_source->id, ), 'Defined class for letters'); my $letter_a = URT::Letter->get(name => 'AAA'); ok($letter_a, 'Got Letter named AAA'); ok($letter_a->score(2), 'Changed score to 2'); my $letter_i = URT::Letter->get(name => 'iii'); ok($letter_i, 'Got letter named iii'); ok($letter_i->name('III'), 'Changed name to III'); my $letter_f = URT::Letter->get(name =>'fff'); ok($letter_f, 'Got letter named fff'); ok($letter_f->delete(), 'Delete letter fff'); my $letter_a2 = URT::Letter->create(id => 10, name => 'aaa', score => 2); ok($letter_a2, 'Created new letter named aaa'); my $letter_a3 = URT::Letter->create(id => 11, name => 'AAA', score => 4); ok($letter_a3, 'Created new letter named aaa'); my $letter_z = URT::Letter->create(id => 12, name => 'zzz', score => 6); ok($letter_z, 'Created new letter named zzz'); ok(UR::Context->commit(), 'Commit changes'); my $fh = IO::File->new($datafile->filename); ok($fh, 'Open data file for reading'); my @lines = <$fh>; is_deeply(\@lines, [ "1\tAAA\t2\n", "11\tAAA\t4\n", "2\tBBB\t1\n", "4\tDDD\t1\n", "5\tEEE\t1\n", "9\tIII\t0\n", "10\taaa\t2\n", "7\tggg\t0\n", "12\tzzz\t6\n", ], 'File contents are correct'); read_columns_from_header.t000444023532023421 1015412121654173 23345 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 21; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my @people = ( Pyle => { rank => 'Private', serial => 123 }, Bailey => { rank => 'Private', serial => 234 }, Hudson => { rank => 'Private', serial => 299 }, Snorkel => { rank => 'Sergent', serial => 345 }, Carter => { rank => 'Sergent', serial => 456 }, Hudson => { rank => 'Sergent', serial => 499 }, Halftrack => { rank => 'General', serial => 567 }, Bob => { rank => 'General', serial => 678 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); for (my $i = 0; $i < @people; $i += 2) { my $name = $people[$i]; my $data = $people[$i+1]; ok(_create_data_file($tmpdir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } ok(UR::Object::Type->define( class_name => 'URT::Soldier', id_by => [ serial => { is => 'Number' } ], has => [ name => { is => 'String' }, rank => { is => 'String' }, ], #data_source => { uri => "file:$tmpdir/\$rank.dat" } data_source => { is => 'UR::DataSource::Filesystem', path => $tmpdir.'/$rank.dat', columns_from_header => 1, header_lines => 2, delimiter => "\t", }, ), 'Defined class for soldiers'); my @objs = URT::Soldier->get(name => 'Pyle', rank => 'Private'); is(scalar(@objs), 1, 'Got one Private named Pyle'); ok(_compare_to_expected($objs[0], { name => 'Pyle', rank => 'Private', serial => 123} ), 'Object has the correct data'); @objs = URT::Soldier->get(rank => 'General'); is(scalar(@objs), 2, 'Got two soldiers with rank General'); ok(_compare_to_expected($objs[0], { name => 'Halftrack', rank => 'General', serial => 567 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Bob', rank => 'General', serial => 678 }), 'Second object has correct data'); @objs = URT::Soldier->get(name => 'no one'); is(scalar(@objs), 0, 'Found no soldiers named "no one"'); @objs = URT::Soldier->get(name => 'Hudson'); is(scalar(@objs), 2, 'Matched two soldiers named Hudson'); ok(_compare_to_expected($objs[0], { name => 'Hudson', rank => 'Private', serial => 299 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Hudson', rank => 'Sergent', serial => 499 }), 'Second object has correct data'); @objs = URT::Soldier->get(456); is(scalar(@objs), 1, 'Got 1 soldier by ID'); ok(_compare_to_expected($objs[0], { name => 'Carter', rank => 'Sergent', serial => 456 }), 'Object has correct data'); sub _compare_to_expected { my($obj,$expected) = @_; return unless $obj->name eq $expected->{'name'}; return unless $obj->id eq $expected->{'serial'}; return unless $obj->serial eq $expected->{'serial'}; return unless $obj->rank eq $expected->{'rank'}; return 1; } my %files; my $files_written = 0; sub _create_data_file { my($dir,$rank,$name,$serial) = @_; my $pathname = $dir . '/' . $rank . '.dat'; my $f = IO::File->new($pathname, '>>'); die "Can't create file $pathname: $!" unless $f; my $write_order; unless (defined( $write_order = $files{$pathname})) { # First time writing to this file. Put in the header # mix it up - half of the files will have name first, half will have serial first $write_order = $files{$pathname} = $files_written % 2; $write_order ? $f->print("name\tserial\n----\t------\n") : $f->print("serial\tname\n------\t----\n"); $files_written++; } $write_order ? $f->print("$name\t$serial\n") : $f->print("$serial\t$name\n"); $f->close; 1; } read_files_as_tables.t000444023532023421 641012121654173 22431 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 25; use IO::File; use File::Temp; use Sub::Install; my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); ok(mkdir($tmpdir."/123/"), 'Create subdir within tmpdir'); my %data = ( 'dogs' => [ [ 1, 'lassie', 11], [ 2, 'benjy', 12 ], [ 3, 'beethoven', 13 ], [ 4, 'ralf', 14 ], ], 'cats' => [ [ 11, 'garfield', 1 ], [ 12, 'nermal', 2 ], [ 13, 'sassy', 3 ], [ 14, 'fluffy', 4 ], ], ); foreach my $species ( keys %data ) { my $pathname = "${tmpdir}/123/${species}.dat"; my $fh = IO::File->new($pathname, 'w') || die "Can't open $pathname for writing: $!"; foreach my $animal ( @{ $data{$species} } ) { $fh->print(join("\t", @$animal) . "\n"); } ok($fh->close(), "wrote info for $pathname"); } my $ds = UR::DataSource::Filesystem->create(path => $tmpdir.'/$group/', columns => ['id','name', 'friend_id'], delimiter => "\t"); ok($ds, 'Created Filesystem datasource'); ok(UR::Object::Type->define( class_name => 'URT::Cat', id_by => [ cat_id => { is => 'Number', column_name => 'id' } ], has => [ group => { is => 'Number' }, name => { is => 'String' }, friend_id => { is => 'Number' }, ], data_source_id => $ds->id, table_name => 'cats.dat' ), 'Defined class for cats'); ok(UR::Object::Type->define( class_name => 'URT::Dog', id_by => [ dog_id => { is => 'Number', column_name => 'id' } ], has => [ group => { is => 'Number' }, name => { is => 'String' }, friend => { is => 'URT::Cat', id_by => 'friend_id' }, friend_name => { via =>'friend', to => 'name' }, ], data_source_id => $ds->id, table_name => 'dogs.dat' ), 'Defined class for dogs'); my @objs = URT::Dog->get(name => 'benjy'); is(scalar(@objs), 1,'Got one dog named benjy'); is($objs[0]->id, 2, 'It has the right id'); is($objs[0]->name, 'benjy', 'It has the right id'); is($objs[0]->friend_id, 12, 'It has the right friend id'); @objs = $objs[0]->friend; is(scalar(@objs), 1, 'it has one friend'); is($objs[0]->id, 12, 'with the right ID'); is($objs[0]->name, 'nermal', 'and the right name'); @objs = URT::Dog->get('id <' => 3); is(scalar(@objs), 2, 'Got 3 dogs with ID < 3'); is($objs[0]->id, 1, 'First has the right ID'); is($objs[1]->id, 2, 'Second has the right ID'); my $cat = URT::Cat->get(name => 'sassy'); ok($cat, 'Got one cat named sassy'); is($cat->name, 'sassy', 'It was the right cat'); @objs = URT::Dog->get(friend => $cat); is(scalar(@objs), 1, 'There is one dog whose friend is sassy'); is($objs[0]->id, 3, 'its ID is correct'); is($objs[0]->name, 'beethoven', 'its name is correct'); @objs = URT::Dog->get(friend_name => 'fluffy'); is(scalar(@objs), 1, 'Got one dog whose friend name is fluffy'); is($objs[0]->id, 4, 'Its ID is correct'); is($objs[0]->name, 'ralf', 'Its name is correct'); read_order_by.t000444023532023421 751512121654173 21126 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 83; use IO::File; use File::Temp; # File data: id name score color my @data = ( [1, 'one', 10,'red'], [2, 'two', 10,'green'], [3, 'three', 9, 'blue'], [4, 'four', 9, 'black'], [5, 'five', 8, 'yellow'], [6, 'six', 8, 'white'], [7, 'seven', 7, 'purple'], [8, 'eight', 7, 'orange'], [9, 'nine', 6, 'pink'], [10, 'ten', 6, 'brown'], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", # handle_class => 'URT::FileTracker', columns => ['thing_id','name','score','color'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Thing', id_by => [ thing_id => { is => 'Number' } ], has => [ name => { is => 'String' }, score => { is => 'Integer' }, color => { is => 'String'}, ], data_source_id => $data_source->id, ), 'Defined class for things'); my @file_columns_in_order = ('id','name','score','color'); my %sorters; foreach my $cols ( [id => 0], [name => 1], [score => 2], [color => 3] ) { my($key,$col) = @$cols; $sorters{$key} = sub { no warnings 'numeric'; $a->[$col] <=> $b->[$col] or $a->[$col] cmp $b->[$col] }; } foreach my $cols ( [id => 0], [name => 1], [score => 2], [color => 3] ) { my($key,$col) = @$cols; $sorters{'-'.$key} = sub { no warnings 'numeric'; $b->[$col] <=> $a->[$col] or $b->[$col] cmp $a->[$col] }; } foreach my $write_sort_order ( 'asc','desc' ) { foreach my $sortby_col ( 0 .. 3 ) { # The number of columns in @data # sort the data by one of the columns... my %file_write_sorters = ( asc => sub { no warnings 'numeric'; $a->[$sortby_col] <=> $b->[$sortby_col] or $a->[$sortby_col] cmp $b->[$sortby_col] }, desc => sub { no warnings 'numeric'; $b->[$sortby_col] <=> $a->[$sortby_col] or $b->[$sortby_col] cmp $a->[$sortby_col] }, ); my $write_sorter = $file_write_sorters{$write_sort_order}; my @write_data = sort $write_sorter @data; ok(save_data_to_file($datafile, \@write_data), "Saved data sorted by column $sortby_col $write_sort_order $file_columns_in_order[$sortby_col]"); $data_source->sorted_columns( [ ($write_sort_order eq 'desc' ? '-' : '') . $data_source->columns->[$sortby_col] ] ); URT::Thing->unload(); my @results = map { [ @$_{@file_columns_in_order} ] } URT::Thing->get(); my $sort_sub = $sorters{'id'}; my @expected = sort $sort_sub @data; is_deeply(\@results, \@expected, 'Got all objects in default (id) sort order'); foreach my $order_by_direction ( '', '-') { for my $sort_prop ( 'id', 'name', 'score', 'color' ) { URT::Thing->unload(); my $order_by_prop = $order_by_direction . $sort_prop; my @results = map { [ @$_{@file_columns_in_order} ] } URT::Thing->get(-order => [ $order_by_prop]); my $sort_sub = $sorters{$order_by_prop}; my @expected = sort $sort_sub @data; is_deeply(\@results, \@expected, "Got all objects sorted by $order_by_prop in the right order"); } } } } sub save_data_to_file { my($fh, $datalist) = @_; $fh->seek(0,0); $fh->print(map { $_ . "\n" } map { join("\t", @$_) } @$datalist); truncate($fh, $fh->tell()); $fh->flush(); return 1; } read_efficiency.t000444023532023421 1620412121654173 21440 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 32; use IO::File; use File::Temp; # The file tracking stuff is defined at the bottom of this file my ($file_new, $file_open, $file_close, $file_DESTROY, $file_seek, $file_seek_pos, $file_tell, $file_getline); IO::File::Tracker->config_callbacks( 'new' => sub { no warnings 'uninitialized'; $file_new++ }, 'open' => sub { no warnings 'uninitialized'; $file_open++ }, 'close' => sub { no warnings 'uninitialized'; $file_close++ }, 'DESTROY' => sub { no warnings 'uninitialized'; $file_DESTROY++ }, 'seek' => sub { no warnings 'uninitialized'; $file_seek_pos = $_[0]; $file_seek++ }, 'tell' => sub { no warnings 'uninitialized'; $file_tell++ }, 'getline' => sub { no warnings 'uninitialized'; $file_getline++ }, ); sub clear_trackers { $file_new = 0; $file_open = 0; $file_close = 0; $file_DESTROY = 0; $file_seek = 0; $file_seek_pos = undef; $file_tell = 0; $file_getline = 0; }; # File data: id name is_upper my @data = ( [1, 'AAA', 1], [2, 'BBB', 1], [3, 'CCC', 1], [4, 'DDD', 1], [5, 'EEE', 1], [6, 'fff', 0], [7, 'ggg', 0], [8, 'hhh', 0], [9, 'iii', 0], ); my $datafile = File::Temp->new(); ok($datafile, 'Created temp file for data'); my $data_source = UR::DataSource::Filesystem->create( path => $datafile->filename, delimiter => "\t", record_separator => "\n", handle_class => 'IO::File::Tracker', columns => ['letter_id','name','is_upper'], ); ok($data_source, 'Create filesystem data source'); ok(UR::Object::Type->define( class_name => 'URT::Letter', id_by => [ letter_id => { is => 'Number' } ], has => [ name => { is => 'String' }, is_upper => { is => 'Boolean' }, ], data_source_id => $data_source->id, ), 'Defined class for letters'); my @file_columns_in_order = ('id','name','is_upper'); my %sorters; foreach my $cols ( [letter_id => 0], [name => 1], [is_upper => 2] ) { my($key,$col) = @$cols; $sorters{$key} = sub { no warnings 'numeric'; $a->[$col] <=> $b->[$col] or $a->[$col] cmp $b->[$col] }; } foreach my $cols ( [letter_id => 0], [name => 1], [is_upper => 2] ) { my($key,$col) = @$cols; $sorters{'-'.$key} = sub { no warnings 'numeric'; $b->[$col] <=> $a->[$col] or $b->[$col] cmp $a->[$col] }; } my($write_sorter, @write_data, @matches, @results, @expected, $sorter_sub); # First, write out the file in id-sorted order. # Don't tell the data source about any particular sorting. &clear_trackers(); $sorter_sub = $sorters{'letter_id'}; @write_data = sort $sorter_sub @data; ok(save_data_to_file($datafile, \@write_data), 'Save file in id-sorted order'); @matches = URT::Letter->get(1); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 1, 'Got one result matching id 1'); is_deeply($results[0], [ 1, 'AAA', 1], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 10, 'getline() was called 10 times'); # One additional at the end of the file is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['letter_id']), 'Configure the data source to be sorted by letter_id'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get(1); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 1, 'Got one result matching id 1'); is_deeply($results[0], [ 1, 'AAA', 1], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 2, 'getline() was called 2 times'); # had to read the 2nd line to know there were no more matches is($file_DESTROY, 1, 'DESTROY was called one time'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('id <' => 5); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 4, 'Got 4 results with id < 5'); is_deeply(\@results, [ [ 1, 'AAA', 1], [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 5, 'getline() was called 5 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['-is_upper']), 'Configure the data source to be sorted by -is_upper'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('is_upper >' => 0); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 5, 'Got 5 results matching is_upper > 0'); is_deeply(\@results, [ [ 1, 'AAA', 1], [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1], [ 5, 'EEE', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 6, 'getline() was called 6 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); ok($data_source->sorted_columns(['name','-is_upper']), 'Configure the data source to be sorted by name and -is_upper'); URT::Letter->unload(); &clear_trackers(); @matches = URT::Letter->get('name between' => ['BBB','DDD']); @results = map { [ @$_{@file_columns_in_order} ] } @matches; is(scalar(@results), 3, 'Got 3 results matching name between BBB and DDD'); is_deeply(\@results, [ [ 2, 'BBB', 1], [ 3, 'CCC', 1], [ 4, 'DDD', 1] ], 'Got the right data back'); is($file_new, 1, 'One new filehandle was created'); is($file_getline, 5, 'getline() was called 5 times'); is($file_DESTROY, 1, 'DESTROY was called one time'); sub save_data_to_file { my($fh, $datalist) = @_; $fh->seek(0,0); $fh->print(map { $_ . "\n" } map { join("\t", @$_) } @$datalist); truncate($fh, $fh->tell()); $fh->flush(); return 1; } package IO::File::Tracker; our %callbacks; sub config_callbacks { my $class = shift; my %set_callbacks = @_; foreach my $key ( keys %set_callbacks) { $callbacks{$key} = $set_callbacks{$key}; } } sub _call_cb { my($op, @args) = @_; my $cb = $callbacks{$op}; if ($cb) { $cb->(@args); } } use vars '$AUTOLOAD'; sub AUTOLOAD { my $subname = $AUTOLOAD; $subname =~ s/^.*:://; my $super = IO::File->can($subname) || IO::Handle->can($subname); if ($super) { $super->(@_); } else { Carp::croak("Can't wrap method $subname because it is not implemented by IO::File"); } } BEGIN { # Create overridden methods for the ones we want to track no strict 'refs'; foreach my $subname (qw( new open close DESTROY seek tell getline ) ) { my $subref = sub { my $self = shift; _call_cb($subname, @_); my $super = IO::File->can($subname); return $super->($self, @_); }; my $fq_subname = 'IO::File::Tracker::'.$subname; *$fq_subname = $subref; } } read.t000444023532023421 710112121654174 17231 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 21; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my @people = ( Hudson => { rank => 'Sergent', serial => 499 }, Bob => { rank => 'General', serial => 678 }, Carter => { rank => 'Sergent', serial => 456 }, Snorkel => { rank => 'Sergent', serial => 345 }, Bailey => { rank => 'Private', serial => 234 }, Halftrack => { rank => 'General', serial => 567 }, Pyle => { rank => 'Private', serial => 123 }, Hudson => { rank => 'Private', serial => 299 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, "Created temp dir $tmpdir"); for (my $i = 0; $i < @people; $i += 2) { my $name = $people[$i]; my $data = $people[$i+1]; ok(_create_data_file($tmpdir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } ok(UR::Object::Type->define( class_name => 'URT::Soldier', id_by => [ serial => { is => 'Number' } ], has => [ name => { is => 'String' }, rank => { is => 'String' }, ], #data_source => { uri => "file:$tmpdir/\$rank.dat" } data_source => { is => 'UR::DataSource::Filesystem', path => $tmpdir.'/$rank.dat', columns => ['name','serial'], delimiter => "\t", }, ), 'Defined class for soldiers'); my @objs = URT::Soldier->get(name => 'Pyle', rank => 'Private'); is(scalar(@objs), 1, 'Got one Private named Pyle'); ok(_compare_to_expected($objs[0], { name => 'Pyle', rank => 'Private', serial => 123} ), 'Object has the correct data'); @objs = URT::Soldier->get(rank => 'General'); is(scalar(@objs), 2, 'Got two soldiers with rank General'); ok(_compare_to_expected($objs[0], { name => 'Halftrack', rank => 'General', serial => 567 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Bob', rank => 'General', serial => 678 }), 'Second object has correct data'); @objs = URT::Soldier->get(name => 'no one'); is(scalar(@objs), 0, 'Found no soldiers named "no one"'); @objs = URT::Soldier->get(name => 'Hudson'); is(scalar(@objs), 2, 'Matched two soldiers named Hudson'); ok(_compare_to_expected($objs[0], { name => 'Hudson', rank => 'Private', serial => 299 }), 'First object has correct data'); ok(_compare_to_expected($objs[1], { name => 'Hudson', rank => 'Sergent', serial => 499 }), 'Second object has correct data'); @objs = URT::Soldier->get(456); is(scalar(@objs), 1, 'Got 1 soldier by ID'); ok(_compare_to_expected($objs[0], { name => 'Carter', rank => 'Sergent', serial => 456 }), 'Object has correct data'); sub _compare_to_expected { my($obj,$expected) = @_; return unless $obj->name eq $expected->{'name'}; return unless $obj->id eq $expected->{'serial'}; return unless $obj->serial eq $expected->{'serial'}; return unless $obj->rank eq $expected->{'rank'}; return 1; } sub _create_data_file { my($dir,$rank,$name,$serial) = @_; my $pathname = $dir . '/' . $rank . '.dat'; my $f = IO::File->new($pathname, '>>'); die "Can't create file $pathname: $!" unless $f; $f->print("$name\t$serial\n"); $f->close; 1; } path_spec_expansion.t000444023532023421 4100212121654174 22366 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 60; use IO::File; use File::Temp; use Sub::Install; # map people to their rank and serial nubmer my %people = ( Pyle => { rank => 'Private', serial => 123 }, Bailey => { rank => 'Private', serial => 234 }, Snorkel => { rank => 'Sergent', serial => 345 }, Carter => { rank => 'Sergent', serial => 456 }, Halftrack => { rank => 'General', serial => 567 }, ); my $tmpdir = File::Temp::tempdir(CLEANUP => 1); ok($tmpdir, 'Created temp dir'); my $tmpdir_strlen = length($tmpdir); my $dir = $tmpdir . '/extra_dir'; ok(mkdir($dir), 'Created extra_dir within temp dir'); my $dir_strlen = length($dir); while (my($name,$data) = each %people) { ok(_create_data_file($dir,$data->{'rank'},$name,$data->{'serial'}), "Create file for $name"); } my $ds = UR::DataSource::Filesystem->create( path => $dir.'/$rank/${name}.dat', columns => ['serial'], ); ok($ds, 'Created data source'); class URT::Thing { has => [ other => { is => 'String' }, other2 => { is => 'String' }, name => { is => 'String' }, rank => { is => 'String' }, serial => { is => 'Number' }, ], data_source_id => $ds->id, }; # First, test the low-level replacement methods for variables # A simple one with single values for both properties my $bx = URT::Thing->define_boolexpr(name => 'Pyle', rank => 'Private'); ok($bx, 'Create boolexpr matching a name and rank'); my @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/$name' ); is(scalar(@data), 1, 'property replacement yielded one pathname'); is_deeply(\@data, [ [ "${dir}/Private/Pyle", { name => 'Pyle', rank => 'Private'} ]], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement yielded one pathname, with extension'); is_deeply(\@data, [ [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private'} ]], 'Path resolution data is correct'); # Give 2 values for each property $bx = URT::Thing->define_boolexpr(rank => ['General','Sergent'], name => ['Pyle','Washington']); ok($bx, 'Create boolexpr matching name and rank with in-clauses'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/$name.dat' ); is(scalar(@data), 4, 'Property replacement yields 4 pathnames'); @data = sort {$a->[0] cmp $b->[0]} @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/General/Washington.dat", { name => 'Washington', rank => 'General' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], [ "${dir}/Sergent/Washington.dat", { name => 'Washington', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This one only supplies a value for one property. It'll have to glob the filesystem for the other value $bx = URT::Thing->define_boolexpr(name => 'Pyle'); ok($bx, 'Create boolexpr with just name'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${dir}.'/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement yielded one pathname, with extension'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "${dir}/*/Pyle.dat", { name => 'Pyle', '.__glob_positions__' => [ [$dir_strlen+1, 'rank' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 3, 'Glob replacement yielded three possible pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This path spec has a hardcoded glob in it already $bx = $bx = URT::Thing->define_boolexpr(name => 'Pyle'); ok($bx, 'Create boolexpr with just name'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/*/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/Pyle.dat", { name => 'Pyle', '.__glob_positions__' => [ [$tmpdir_strlen+3, 'rank' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 3, 'Glob replacement yielded three possible pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Pyle.dat", { name => 'Pyle', rank => 'General' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Pyle.dat", { name => 'Pyle', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # Make a bx with no filters and two properties in the path spec $bx = $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/*/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+3, 'rank' ],[$tmpdir_strlen+5,'name' ] ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # a bx with no filters and three properties in the path spec $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/$other/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+1, 'other' ], [$tmpdir_strlen+3,'rank'], [$tmpdir_strlen+5,'name' ], ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra_dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra_dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra_dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra_dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra_dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # This one has multiple variables in the same path portion $bx = URT::Thing->define_boolexpr(); ok($bx, 'Create boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_vars_with_values_in_pathname( $bx, ${tmpdir}.'/${other}_${other2}/$rank/${name}.dat' ); is(scalar(@data), 1, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/*_*/*/*.dat", { '.__glob_positions__' => [ [$tmpdir_strlen+1, 'other' ], [$tmpdir_strlen+3, 'other2' ], [$tmpdir_strlen+5,'rank'], [$tmpdir_strlen+7,'name' ], ] } ] ], 'Path resolution data is correct'); @data = UR::DataSource::Filesystem->_replace_glob_with_values_in_pathname(@{$data[0]}); is(scalar(@data), 5, 'Glob replacement yielded five possible pathname'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', other2 => 'dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', other2 => 'dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', other2 => 'dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', other2 => 'dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', other2 => 'dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # Try it on a method call my $is_sub_called = 0; my $bx_from_sub; my $class_from_sub; my $resolver = sub { my($class,$rule) = @_; $class_from_sub = $class; $bx_from_sub = $bx; $is_sub_called++; return 'extra_dir'; }; Sub::Install::install_sub({ code => $resolver, into => 'URT::Thing', as => 'extra_path_resolver' }); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/General/Halftrack.dat' ); is(scalar(@data), 1, 'property replacement for spec including a method call yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); is($is_sub_called, 1, 'The resolver sub was called'); is($class_from_sub, 'URT::Thing', 'The resolver sub was passed the right class name'); is($bx_from_sub, $bx, 'The resolver sub was passed the right boolexpr'); # pair of method calls Sub::Install::install_sub({ code => sub { 'dat' }, into => 'URT::Thing', as => 'data_file_extension'}); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/General/Halftrack.&data_file_extension' ); is(scalar(@data), 1, 'property replacement for spec including two method calls yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # pair of methods in the same path part Sub::Install::install_sub({ code => sub { 'extra' }, into => 'URT::Thing', as => 'extra_word'}); Sub::Install::install_sub({ code => sub { 'dir' }, into => 'URT::Thing', as => 'dir_word'}); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&{extra_word}_&{dir_word}/General/Halftrack.&data_file_extension' ); is(scalar(@data), 1, 'property replacement for spec including three yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/Halftrack.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # method call returning multiple values Sub::Install::install_sub({ code => sub { return ('General','Private','Sergent') }, into => 'URT::Thing', as => 'rank_list'}); $bx = URT::Thing->define_boolexpr(); ok($bx, 'Created boolexpr with no filters'); @data = UR::DataSource::Filesystem->_replace_subs_with_values_in_pathname( $bx, ${tmpdir}.'/&extra_path_resolver/&rank_list/*.&data_file_extension' ); is(scalar(@data), 3, 'property replacement for spec including a glob yielded one pathname'); #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "$tmpdir/extra_dir/General/*.dat", {'.__glob_positions__' => []} ], [ "$tmpdir/extra_dir/Private/*.dat", {'.__glob_positions__' => []} ], [ "$tmpdir/extra_dir/Sergent/*.dat", {'.__glob_positions__' => []} ] ], 'Path resolution data is correct'); # put it all together # a bunch of variables $bx = URT::Thing->define_boolexpr(); @data = UR::DataSource::Filesystem->resolve_file_info_for_rule_and_path_spec($bx, ${tmpdir}.'/${other}_${other2}/$rank/${name}.dat'); is(scalar(@data), 5, 'resolve_file_info_for_rule_and_path_spec() returns 5 pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', other2 => 'dir', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', other2 => 'dir', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', other2 => 'dir', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', other2 => 'dir', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', other2 => 'dir', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); # variables, methods and globs $bx = URT::Thing->define_boolexpr(); @data = UR::DataSource::Filesystem->resolve_file_info_for_rule_and_path_spec( $bx, ${tmpdir}.'/${other}_&dir_word/$rank/${name}.&data_file_extension' ); is(scalar(@data), 5, 'resolve_file_info_for_rule_and_path_spec() returns 5 pathnames'); @data = sort { $a->[0] cmp $b->[0] } @data; #print Data::Dumper::Dumper(\@data); is_deeply(\@data, [ [ "${dir}/General/Halftrack.dat", { other => 'extra', name => 'Halftrack', rank => 'General' } ], [ "${dir}/Private/Bailey.dat", { other => 'extra', name => 'Bailey', rank => 'Private' } ], [ "${dir}/Private/Pyle.dat", { other => 'extra', name => 'Pyle', rank => 'Private' } ], [ "${dir}/Sergent/Carter.dat", { other => 'extra', name => 'Carter', rank => 'Sergent' } ], [ "${dir}/Sergent/Snorkel.dat", { other => 'extra', name => 'Snorkel', rank => 'Sergent' } ], ], 'Path resolution data is correct'); 1; sub _create_data_file { my($dir,$rank,$name,$data) = @_; my $subdir = $dir . '/' . $rank; unless (-d $subdir) { mkdir $subdir || die "Can't create subdir $subdir: $!"; } my $pathname = $subdir . '/' . $name . '.dat'; my $f = IO::File->new($pathname, 'w') || die "Can't create file $pathname: $!"; $f->print($data); 1; } read_multichar_record_sep.t000444023532023421 357112121654174 23515 0ustar00abrummetgsc000000000000UR-0.41/t/URT/t/file_datasource#!/usr/bin/env perl use strict; use warnings; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../../lib"; use lib File::Basename::dirname(__FILE__)."/../../.."; use URT; use Test::More tests => 8; use IO::File; use File::Temp; use Sub::Install; # Test the case where the record separator is a multi-character string, and # make sure that the last record in the file does not match that whole string # Make a FASTQ-like format file intentionally missing a blank line at the end my $fh = File::Temp->new(); my $data = "read1 ACGTTGCA + 12345678 abc read2 GAAGTCCT + 87654321 a"; $fh->print($data); $fh->close; ok(UR::Object::Type->define( class_name => 'URT::FastqReads', id_by => [ path => { is => 'String', column_name => '__FILE__' }, record => { is => 'Integer', column_name => '$.' }, ], has => [ seq_id => { is => 'String'}, sequence => { is => 'String' }, quality => { is => 'String' }, ], data_source => { is => 'UR::DataSource::Filesystem', path => '$path', columns => ['seq_id','sequence', undef, 'quality'], delimiter => "\n", record_separator => "\nabc\n", }, ), 'Defined class for fastq reads'); my @objs = URT::FastqReads->get(path => $fh->filename); is(scalar(@objs), 2, 'Read in 1 records from the fastq file'); my @expected = ( { seq_id => 'read1', sequence => 'ACGTTGCA', quality => '12345678' }, { seq_id => 'read2', sequence => 'GAAGTCCT', quality => '87654321' }, ); for (my $i = 0; $i < @expected; $i++) { _compare_to_expected($objs[$i], $expected[$i]); } sub _compare_to_expected { my($obj,$expected) = @_; foreach my $prop ( keys %$expected ) { is($obj->$prop, $expected->{$prop}, "property $prop is correct"); } return 1; } DataSource000755023532023421 012121654175 14434 5ustar00abrummetgsc000000000000UR-0.41/t/URTSomeMySQL.pm000444023532023421 116712121654172 16722 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomeMySQL; use URT; class URT::DataSource::SomeMySQL { is => ['UR::DataSource::MySQL'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { 'dbname=somemysql;host='; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { undef; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; Meta.pm000444023532023421 214712121654173 16017 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSourcepackage URT::DataSource::Meta; # The datasource for metadata describing the tables, columns and foreign # keys in the target datasource use strict; use warnings; use UR; UR::Object::Type->define( class_name => 'URT::DataSource::Meta', is => ['UR::DataSource::Meta'], ); use File::Temp; # Override server() so we can make the metaDB file in # a temp dir sub server { my $self = shift; our $PATH; $PATH ||= File::Temp::tmpnam() . "_ur_testsuite_metadb" . $self->_extension_for_db; return $PATH; } # Don't print out warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::SomeSQLite sub _dont_emit_initializing_messages { my($dsobj, $message) = @_; if ($message =~ m/^Re-creating/) { # don't emit the message about re-creating the DB when run in the test harness $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { our $PATH; unlink $PATH if ($PATH); } 1; SomeFileMux.pm000444023532023421 142712121654174 17327 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource package URT::DataSource::SomeFileMux; use strict; use warnings; use UR::Object::Type; use URT; use File::Temp qw(); class URT::DataSource::SomeFileMux { is => ['UR::DataSource::FileMux', 'UR::Singleton'], }; sub constant_values { [ 'thing_type' ] } sub required_for_get { [ 'thing_type' ] } sub column_order { return [ qw( thing_id thing_name thing_color )]; } sub sort_order { return ['thing_id' ] ; } sub delimiter { "\t" } BEGIN { our $BASE_PATH = File::Temp::tempdir( CLEANUP => 1 ); } # Note that the file resolver is called as a normal function (with the parameters # mentioned in requiret_for_get), not as a method with the data source as the # first arg... sub file_resolver { my $type = shift; our $BASE_PATH; return "$BASE_PATH/$type"; } 1; Meta.sqlite3-schema000444023532023421 422212121654174 20222 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; Meta.sqlite3000444023532023421 4600012121654174 17004 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSourceSQLite format 3@         / ++Qtabledd_bitmap_indexdd_bitmap_indexCREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) )=Q+indexsqlite_autoindex_dd_bitmap_index_1dd_bitmap_index --Mtabledd_fk_constraintdd_fk_constraintCREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) )?S-indexsqlite_autoindex_dd_fk_constraint_1dd_fk_constraint )x;;Qtabledd_fk_constraint_columndd_fk_constraint_columnCREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) )Ma;indexsqlite_autoindex_dd_fk_constraint_column_1dd_fk_constraint_column6;;tabledd_pk_constraint_columndd_pk_constraint_column CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) )Ma;indexsqlite_autoindex_dd_pk_constraint_column_1dd_pk_constraint_column     2cc tabledd_tabledd_table CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) )/ Cindexsqlite_autoindex_dd_table_1dd_table  ++ktabledd_table_columndd_table_columnCREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) )     N= Q+indexsqlite_autoindex_dd_table_column_1dd_table_columnX CC7tabledd_unique_constraint_columndd_unique_constraint_columnCREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) )UiCindexsqlite_autoindex_dd_unique_constraint_column_1dd_unique_constraint_columnSomeSQLite.pm000444023532023421 263212121654174 17116 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource package URT::DataSource::SomeSQLite; use strict; use warnings; use File::Temp; BEGIN { my $fh = File::Temp->new(TEMPLATE => 'ur_testsuite_db_XXXX', UNLINK => 0, SUFFIX => '.sqlite3', OPEN => 0, TMPDIR => 1); our $FILE = $fh->filename(); $fh->close(); # The DB file now exists with 0 size our $DUMP_FILE = File::Temp::tmpnam(); } use UR::Object::Type; use URT; class URT::DataSource::SomeSQLite { is => ['UR::DataSource::SQLite','UR::Singleton'], }; # Don't print warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::Meta. sub _dont_emit_initializing_messages { my($dsobj, $msg) = @_; if ($msg =~ m/^Re-creating|Skipped unload/) { $_[1] = undef; # don't print the message } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { my @paths_to_remove = map { __PACKAGE__->$_ } qw(server _data_dump_path _schema_path); unlink(@paths_to_remove); } # Standard behavior is to put the DB file right next to the module # We'll change that to point to the temp file sub server { our $FILE; return $FILE; } sub _data_dump_path { our $DUMP_FILE; return $DUMP_FILE; } 1; Meta.sqlite3-dump000444023532023421 425512121654174 17735 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSourcePRAGMA foreign_keys = OFF; BEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; SomeOracle.pm000444023532023421 114112121654174 17154 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomeOracle; use URT; class URT::DataSource::SomeOracle { is => ['UR::DataSource::Oracle'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { ''; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { ''; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; SomeFile.pm000444023532023421 64612121654174 16617 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSourcepackage URT::DataSource::SomeFile; use strict; use warnings; use URT; use File::Temp qw(); our(undef, $FILE) = File::Temp::tempfile(); END { unlink $FILE }; class URT::DataSource::SomeFile { is => ['UR::Singleton', 'UR::DataSource::File'], }; sub server { $FILE } sub column_order { return [ qw( thing_id thing_name thing_color ) ]; } sub sort_order { return ['thing_id']; } sub delimiter { "\t" } 1; CircFk.pm000444023532023421 45112121654175 16250 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource package URT::DataSource::CircFk; use strict; use warnings; use UR::Object::Type; use URT; class URT::DataSource::CircFk { is => ['UR::DataSource::SQLite'], }; our $FILE = "/tmp/ur_testsuite_db_$$.sqlite"; IO::File->new($FILE, 'w')->close(); END { unlink $FILE } sub server { $FILE } 1; SomePostgreSQL.pm000444023532023421 120612121654175 17755 0ustar00abrummetgsc000000000000UR-0.41/t/URT/DataSource use strict; use warnings; package URT::DataSource::SomePostgreSQL; use URT; class URT::DataSource::SomePostgreSQL { is => ['UR::DataSource::Pg'], }; # This becomes the third part of the colon-separated data_source # string passed to DBI->connect() sub server { 'dbname=somepostgresql;host='; } # This becomes the schema argument to most of the data dictionary methods # of DBI like table_info, column_info, etc. sub owner { 'public'; } # This becomes the username argument to DBI->connect sub login { ''; } # This becomes the password argument to DBI->connect sub auth { ''; } 1; Context000755023532023421 012121654172 14023 5ustar00abrummetgsc000000000000UR-0.41/t/URTTesting.pm000444023532023421 43412121654172 16114 0ustar00abrummetgsc000000000000UR-0.41/t/URT/Contextpackage URT::Context::Testing; use strict; use warnings; use UR::Object::Type; use URT; class URT::Context::Testing { is => ['UR::Context::Root'], doc => 'Used by the automated test suite.', }; sub get_default_data_source { "GSC::DataSource::SomeSQLite" } 1; #$Header Vending000755023532023421 012121654175 13322 5ustar00abrummetgsc000000000000UR-0.41/tCoin.pm000444023532023421 130612121654172 14702 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Coin; use strict; use warnings; use Vending; class Vending::Coin { table_name => 'COIN', is => 'Vending::Content', id_by => [ coin_id => { is => 'integer' }, ], has => [ name => { via => 'item_type', to => 'name' }, value_cents => { via => 'coin_type', to => 'value_cents' }, coin_type => { is => 'Vending::CoinType', id_by => 'name' }, item_type => { is => 'Vending::ContentType', id_by => 'type_id', constraint_name => 'COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; sub subtype_name_resolver { return __PACKAGE__; } 1; vend000555023532023421 21312121654172 14312 0ustar00abrummetgsc000000000000UR-0.41/t/Vending#!/gsc/bin/perl use strict; use warnings; use above 'UR'; use above "Vending"; Vending::Command->execute_with_shell_params_and_exit(); Product.pm000444023532023421 160712121654172 15436 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Product; use strict; use warnings; use Vending; class Vending::Product { is => [ 'Vending::ContentType' ], table_name => 'PRODUCT', id_sequence_generator_name => 'URMETA_content_type_TYPE_ID_seq', id_by => [ product_id => { is => 'integer' }, ], has => [ manufacturer => { is => 'varchar' }, cost_cents => { is => 'integer' }, price => { calculate_from => 'cost_cents', calculate => q(sprintf("\$%.2f", $cost_cents/100)), doc => 'display price in dollars' }, item_type_product => { is => 'Vending::ContentType', id_by => 'product_id', constraint_name => 'PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'kinds of things the machine sells', }; 1; MachineLocation.pm000444023532023421 460612121654173 17056 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::MachineLocation; use strict; use warnings; use Vending; class Vending::MachineLocation { table_name => 'MACHINE_LOCATION', id_by => [ machine_location_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, label => { is => 'varchar', is_optional => 1 }, is_buyable => { is => 'integer' }, cost_cents => { is => 'integer', is_optional => 1 }, items => { is => 'Vending::Content', reverse_as => 'machine_location', is_many => 1 }, coins => { is => 'Vending::Coin', reverse_as => 'machine_location', is_many => 1 }, count => { calculate => q(my @obj = $self->items; return scalar(@obj);), doc => 'How many items are in this machine_location' }, content_value => { calculate => q(my @obj = $self->items; my $val = 0; $val += $_->isa('Vending::Coin') ? $_->value_cents : $_->cost_cents foreach @obj; return $val;), doc => 'Value of all the items in this machine_location' }, content_value_dollars => { calculate_from => 'content_value', calculate => q(sprintf("\$%.2f", $content_value/100)), doc => 'Value of all the contents in dollars' }, price => { calculate_from => 'cost_cents', calculate => q(sprintf("\$%.2f", $cost_cents/100)), doc => 'display price in dollars' }, machine => { is => 'Vending::Machine', id_by => 'machine_id', constraint_name => 'MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK' }, machine_id => { is => 'integer' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'represents a "machine_location" in the machine, such as "A", "B", "user","change"', }; sub transfer_items_to_machine_location { my($self,$to_machine_location) =@_; my $to_machine_location_id = $to_machine_location->id; my @objects = $self->items(); $_->machine_location_id($to_machine_location_id) foreach @objects; return scalar(@objects); } 1; Command.pm000444023532023421 53412121654173 15353 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Command; use strict; use warnings; use Vending; class Vending::Command { is => 'Command', has => [ machine_id => { default_value => 1 }, machine => { is => 'Vending::Machine', id_by => 'machine_id' }, ], }; #sub machine { # my $machine = Vending::Machine->get(); # return $machine; #} 1; machine_classes_1.uxf000444023532023421 2341712121654173 17571 0ustar00abrummetgsc000000000000UR-0.41/t/Vending com.umlet.element.base.Relation 130 200 260 220 lt=<<- //subject_id:Vending::VendSlot;UR::Entity;; 240;200;20;20 com.umlet.element.base.Relation 495 320 340 120 lt=<<- //subject_id:UR::Singleton;UR::Object;; 20;100;320;20 com.umlet.element.base.Class 420 180 290 160 Vending::Coin -- value_cents: name: String +coin_id: integer item_type: Vending::ItemType coin_type: Vending::CoinType type_id: integer subtype_name: //subject_id:Vending::Coin com.umlet.element.base.Relation 545 120 325 240 lt=<<- //subject_id:Vending::Coin;Vending::VendItem;; 305;20;20;220 com.umlet.element.base.Relation 260 272 510 88 lt=<- //subject_id:Vending::VendSlot;UR::Singleton;machine_id;id 490;68;20;20 com.umlet.element.base.Class 720 20 290 100 Vending::CoinType -- type_id: value_cents: integer item_type: Vending;:ItemType +name: String //subject_id:Vending::CoinType com.umlet.element.base.Class 320 380 100 20 UR::Entity //subject_id:UR::Entity com.umlet.element.base.Relation 260 326 1080 142 lt=<- //subject_id:Vending::ReturnedItem;Vending::VendSlot;source_slot_id;slot_id 20;20;1060;122 com.umlet.element.base.Class 20 220 260 280 Vending::VendSlot -- items: Vending::VendItem label: varchar content_value: machine_id: Scalar cost_cents: integer content_value_dollars: +slot_id: integer count: coins: Vending::Coin price: machine: UR::Singleton is_buyable: integer name: varchar //subject_id:Vending::VendSlot com.umlet.element.base.Relation 180 0 655 380 lt=<<- //subject_id:Vending::Machine;UR::Singleton;; 635;360;20;20 com.umlet.element.base.Class 1050 60 220 100 Vending::Product -- price: +product_id: integer manufacturer: varchar cost_cents: integer //subject_id:Vending::Product com.umlet.element.base.Class 1320 340 310 140 Vending::ReturnedItem -- cost_cents: price: value: Float source_slot: Vending::VendSlot name: String source_slot_id: integer //subject_id:Vending::ReturnedItem com.umlet.element.base.Class 720 140 260 160 Vending::VendItem -- machine_id: Scalar slot: Vending::VendSlot machine: Vending::Machine subtype_name: varchar +vend_item_id: integer slot_id: integer slot_name: //subject_id:Vending::VendItem com.umlet.element.base.Class 750 340 130 20 UR::Singleton //subject_id:UR::Singleton com.umlet.element.base.Relation 830 120 340 280 lt=<<- //subject_id:Vending::Merchandise;Vending::VendItem;; 20;20;320;260 com.umlet.element.base.Class 420 20 260 120 Vending::ItemType -- machine_id: Scalar machine: Vending::Machine name: varchar count: +type_id: integer //subject_id:Vending::ItemType com.umlet.element.base.Relation 690 72 50 164 lt=<- //subject_id:Vending::Coin;Vending::CoinType;name;name 30;20;20;144 com.umlet.element.base.Relation 350 360 185 80 lt=<<- //subject_id:UR::Entity;UR::Object;; 165;60;20;20 com.umlet.element.base.Relation 350 0 220 420 lt=<<- //subject_id:Vending::ItemType;UR::Entity;; 20;400;200;20 com.umlet.element.base.Relation 260 228 480 138 lt=<- //subject_id:Vending::VendItem;Vending::VendSlot;slot_id;slot_id 20;118;460;20 com.umlet.element.base.Relation 360 0 80 58 lt=<- //subject_id:Vending::ItemType;Vending::Machine;machine_id;id 20;20;60;38 com.umlet.element.base.Class 20 20 360 180 Vending::Machine -- items: Vending::VendItem coin_box_slot: products: Vending::Product bank_slot: change_dispenser: slots: Vending::VendSlot inventory_items: Vending::Merchandise item_types: Vending::ItemType //subject_id:Vending::Machine com.umlet.element.base.Relation 495 320 1000 120 lt=<<- //subject_id:Vending::ReturnedItem;UR::Object;; 20;100;980;20 com.umlet.element.base.Relation 1030 76 270 286 lt=<- //subject_id:Vending::Merchandise;Vending::Product;product_id;product_id 20;20;250;266 com.umlet.element.base.Relation 400 90 40 218 lt=<- //subject_id:Vending::Coin;Vending::ItemType;type_id;type_id 20;20;20;198 com.umlet.element.base.Relation 350 0 535 420 lt=<<- //subject_id:Vending::CoinType;UR::Entity;; 20;400;515;20 com.umlet.element.base.Relation 530 0 650 180 lt=<<- //subject_id:Vending::Product;Vending::ItemType;; 20;20;630;160 com.umlet.element.base.Class 1020 180 260 200 Vending::Merchandise -- manufacturer: name: product: Vending::Product +inv_id: integer cost_cents: insert_date: datetime price: subtype_name: product_id: integer //subject_id:Vending::Merchandise com.umlet.element.base.Relation 360 0 380 178 lt=<- //subject_id:Vending::VendItem;Vending::Machine;machine_id;id 20;20;360;158 com.umlet.element.base.Class 460 380 110 40 UR::Object -- +id: Scalar //subject_id:UR::Object com.umlet.element.base.Relation 350 120 520 300 lt=<<- //subject_id:Vending::VendItem;UR::Entity;; 20;280;500;20 vend_interactive.pl000555023532023421 327512121654173 17355 0ustar00abrummetgsc000000000000UR-0.41/t/Vending#!/gsc/bin/perl use above 'Vending'; my $machine = Vending::Machine->get(); unless ($machine) { print STDERR "Out of order...\n"; exit; } my %command_map = ( help => \&help, done => \&done, 'check-again' => \&clear_query_cache, 'coin-return' => 'Vending::Command::CoinReturn', dollar => 'Vending::Command::Dollar', quarter => 'Vending::Command::Quarter', dime => 'Vending::Command::Dime', nickel => 'Vending::Command::Nickel', buy => 'Vending::Command::Buy', menu => 'Vending::Command::Menu', ); $|=1; &help(); while (1) { print "command> "; my $line = <>; last unless $line; chomp($line); my @words = split(/\s+/, $line); my $thing = $command_map{shift @words}; if (ref($thing)) { # It's a sub we can just call $thing->(); } elsif($thing) { # It's a command class name my $command = $thing->create(bare_args => \@words); if ($command->execute() ) { UR::Context->commit(); } } else { print "That is not a valid command\n"; } } &done(); sub done { print "\nGoodbye\n"; exit(0); } sub help { print q( Vendco Vending Machine available commands: dollar - insert a dollar quarter - insert a quarter dime - insert a dime nickel - insert a nickel menu - See what is available buy - purchase an item from the menu coin-return - return any coins you inserted help - this help text check-again - secret backdoor to use when another progrtam reloads the inventory ); } sub clear_query_cache { print "Forgetting about Vending::Merchandises and Vending::Coins\n"; Vending::Merchandise->unload(); Vending::Coin::Change->unload(); } CoinType.pm000444023532023421 130712121654173 15546 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::CoinType; use strict; use warnings; use Vending; class Vending::CoinType { table_name => 'coin_type', id_by => [ name => { is => 'String' }, ], has => [ value_cents => { is => 'integer' }, item_type => { is => 'Vending::ItemType', where => [ name => \'name'] }, type_id => { via => 'item_type' }, ], doc => 'kinds of coins the machine accepts, and their value', data_source => 'Vending::DataSource::CoinType', }; # Overriding because the property definition doesn't exactly work... sub item_type { my $self = shift; my $type_obj = Vending::ContentType->get_or_create(name => $self->name); return $type_obj; } 1; ReturnedItem.pm000444023532023421 333412121654173 16425 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::ReturnedItem; use strict; use warnings; use Vending; class Vending::ReturnedItem { has => [ name => { is => 'String' }, value => { is => 'Float' }, source_machine_location => { is => 'Vending::MachineLocation', id_by => 'source_machine_location_id' }, price => { via => 'source_machine_location', to => 'price' }, cost_cents => { via => 'source_machine_location', to => 'cost_cents' }, ], doc => 'Represents a thing being returned to the user, not stored in the database', }; # Create Vending::ReturnedItem objects from a product or coin # To enforce vending machine rules, the passed-in item is deleted sub create_from_vend_items { my($class,@items) = @_; my $transaction = UR::Context::Transaction->begin(); my @returned_items = eval { my @returned_items; foreach my $item ( @items ) { my %create_params = ( name => $item->name, source_machine_location_id => $item->machine_location_id ); if ($item->isa('Vending::Coin')) { $create_params{'value'} = $item->value_cents; } elsif ($item->isa('Vending::Merchandise')) { $create_params{'value'} = $item->cost_cents; } else { die "Can't create a Vending::ReturnedItem from an object of type ".$item->class; } $item->delete(); my $returned_item = $class->create(%create_params); push @returned_items, $returned_item; } return @returned_items; }; if ($@) { $class->error_message($@); $transaction->rollback(); } else { $transaction->commit(); return @returned_items; } } 1; Content.pm000444023532023421 330012121654173 15421 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Content; use strict; use warnings; use Vending; class Vending::Content { table_name => 'CONTENT', is_abstract => 1, subclassify_by => 'subtype_name', id_by => [ content_id => { }, ], has => [ machine => { is => 'Vending::Machine', id_by => 'machine_id', constraint_name => 'CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK' }, machine_id => { value => '1', is_constant => 1, is_classwide => 1, column_name => '' }, machine_location_id => { is => 'integer' }, subtype_name => { is => 'varchar', is_optional => 1 }, machine_location => { is => 'Vending::MachineLocation', id_by => 'machine_location_id', constraint_name => 'CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK' }, location_name => { via => 'machine_location', to => 'name' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; # Called when you try to create a generic Vending::Content sub subtype_name_resolver { my $class = shift; my %params; if (ref($_[0])) { %params = %{$_[0]}; # Called with obj as arg } else { %params = @_; # called with hash as arglist } return $params{'subtype_name'}; } # Turn this thing into a Vending::ReturnedItem to give back to the user # as a side effect, $self is deleted sub dispense { my $self = shift; my @items_to_dispense; if (ref($self)) { # object method... @items_to_dispense = ($self); } else { # Class method @items_to_dispense = @_; } return Vending::ReturnedItem->create_from_vend_items(@items_to_dispense); } 1; notes.txt000444023532023421 355012121654174 15352 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingur define namespace Vending ur define datasource sqlite --dsname Machine Schema: -- name the kinds of things the machine knows about create table item_type (type_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL); -- places where things get sloted in create table vend_slot (slot_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, is_buyable integer NOT NULL, cost_cents integer, label varchar); -- kinds of coins we'll accept and their value --create table coin_type(type_id integer PRIMARY KEY NOT NULL REFERENCES item_type(type_id), -- value_cents integer NOT NULL); --Parent table for instances of things the machine can sell create table vend_item (vend_item_id integer PRIMARY KEY NOT NULL, subtype_name varchar, slot_id integer NOT NULL REFERENCES vend_slot(slot_id)); -- instances of coins held by the machine create table coin (coin_id integer PRIMARY KEY NOT NULL REFERENCES vend_item(vend_item_id), type_id integer NOT NULL REFERENCES item_type(type_id)); -- kinds of things we'll sell create table product (product_id integer PRIMARY KEY NOT NULL REFERENCES item_type(type_id), cost_cents integer NOT NULL, manufacturer varchar NOT NULL); -- instances of things in the inventory create table inventory (inv_id integer PRIMARY KEY NOT NULL, product_id integer NOT NULL REFERENCES product(product_id), insert_date datetime NOT NULL DEFAULT (date('now'))); ur update classes fixup VendingMachine::Inventory add indirect properties for name, price, is_sellable make command line script vend Make skeleton VendingMachine::Command Machine.pm000444023532023421 1315212121654174 15402 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Machine; use strict; use warnings; use Vending; class Vending::Machine { table_name => 'MACHINE', id_by => [ machine_id => { is => 'Integer' }, ], has => [ coin_box => { via => 'machine_locations', to => '-filter', where => [ name => 'box' ] }, bank => { via => 'machine_locations', to => '-filter', where => [ name => 'bank' ] }, change_dispenser => { via => 'machine_locations', to => '-filter', where => [ name => 'change' ] }, address => { is => 'Text', is_optional => 1 }, ], has_many => [ products => { is => 'Vending::Product', reverse_as => 'machine' }, items => { is => 'Vending::Content', reverse_as => 'machine' }, inventory_items => { is => 'Vending::Merchandise', reverse_as => 'machine' }, item_types => { is => 'Vending::ContentType', reverse_as => 'machine' }, machine_locations => { is => 'Vending::MachineLocation', reverse_as => 'machine' }, ], data_source => 'Vending::DataSource::Machine', }; sub insert { my($self, $item_name) = @_; my $coin_type = Vending::CoinType->get(name => $item_name); unless ($coin_type) { $self->error_message("This machine does not accept '$item_name' coins"); return; } my $loc = $self->coin_box(); my $coin = $loc->add_coin(type_id => $coin_type->type_id, machine_id => $self); return defined($coin); } sub coin_return { my $self = shift; my $loc = $self->coin_box; my @coins = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@coins); return @returned_items; } sub empty_bank { my $self = shift; my $loc = $self->bank(); my @coins = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@coins); return @returned_items; } sub empty_machine_location_by_name { my($self,$name) = @_; my $loc = $self->machine_locations(name => $name); return unless $loc; unless ($loc->is_buyable) { die "You can only empty out inventory type machine_locations"; } my @items = $loc->items(); my @returned_items = Vending::ReturnedItem->create_from_vend_items(@items); return @returned_items; } sub buy { my($self,@machine_location_names) = @_; my $coin_box = $self->coin_box(); my $transaction = UR::Context::Transaction->begin(); my @returned_items = eval { my $users_money = $coin_box->content_value(); my @bought_items; my %iterator_for_machine_location; foreach my $loc_name ( @machine_location_names ) { my $machine_location = $self->machine_locations(name => $loc_name); unless ($machine_location && $machine_location->is_buyable) { die "$loc_name is not a valid choice\n"; } my $iter = $iterator_for_machine_location{$loc_name} || $machine_location->item_iterator(); unless ($iter) { die "Problem creating iterator for $loc_name\n"; return; } my $item = $iter->next(); # This is the one they'll buy unless ($item) { $self->error_message("Item $loc_name is empty"); next; } push @bought_items, $item->dispense; } my @change; if (@bought_items) { @change = $self->_complete_purchase_and_make_change_for_selections(@bought_items); } return (@change,@bought_items); }; if ($@) { my($error) = ($@ =~ m/^(.*?)\n/); $self->error_message("Couldn't process your purchase:\n$error"); $transaction->rollback(); return; } else { $transaction->commit(); return @returned_items; } } # Note that this will die if there's a problem making change sub _complete_purchase_and_make_change_for_selections { my($self,@bought_items) = @_; my $coin_box = $self->coin_box(); my $purchased_value = 0; foreach my $item ( @bought_items ) { $purchased_value += $item->cost_cents; } my $change_value = $coin_box->content_value() - $purchased_value; if ($change_value < 0) { die "You did not enter enough money\n"; } # Put all the user's coins into the bank my $bank = $self->bank; $coin_box->transfer_items_to_machine_location($bank); if ($change_value == 0) { return; } # List of coin types in decreasing value my @available_coin_types = map { $_->name } sort { $b->value_cents <=> $a->value_cents } Vending::CoinType->get(); my $change_dispenser = $self->change_dispenser; my @change; # Make change for the user MAKING_CHANGE: foreach my $coin_name ( @available_coin_types ) { my $coin_iter = $change_dispenser->coin_iterator(name => $coin_name); unless ($coin_iter) { die "Can't create iterator for Vending::Coin::Change\n"; } THIS_coin_type: while ( my $coin = $coin_iter->next() ) { last if $change_value < $coin->value_cents; my($change_coin) = $coin->dispense; $change_value -= $change_coin->value; push @change, $change_coin; } } if ($change_value) { #$DB::single=1; die "Not enough change\n"; } return @change; } sub _initialize_for_tests { my $self = shift; $_->delete foreach $self->inventory_items(); $_->delete foreach $self->products(); $_->delete foreach $self->items(); } 1; Merchandise.pm000444023532023421 171312121654174 16240 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Merchandise; use strict; use warnings; use Vending; class Vending::Merchandise { is => [ 'Vending::Content' ], table_name => 'merchandise', id_sequence_generator_name => 'URMETA_coin_coin_ID_seq', id_by => [ merchandise_id => { is => 'integer' }, ], has => [ product => { is => 'Vending::Product', id_by => 'product_id', constraint_name => 'inventory_product_ID_product_product_ID_FK' }, insert_date => { is => 'datetime' }, product_id => { is => 'integer', implied_by => 'product' }, name => { via => 'product' }, cost_cents => { via => 'product' }, price => { via => 'product' }, manufacturer => { via => 'product' }, ], schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', doc => 'instances of things the machine will sell and dispense', }; sub subtype_name_resolver { return __PACKAGE__; } 1; Vocabulary.pm000444023532023421 20612121654174 16101 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::Vocabulary; use warnings; use strict; use UR; class Vending::Vocabulary { is => [ 'UR::Vocabulary' ], }; 1; get_coin_by_value.pl000444023532023421 57712121654175 17462 0ustar00abrummetgsc000000000000UR-0.41/t/Vendinguse above 'Vending'; # Requires a 3-table database join (COIN, CONTENT and CONTENT_TYPE), plus a # cross-datasource join to Vending::CoinType my @coins = Vending::Coin->get(value_cents => 25); print "Found ",scalar(@coins)," coins:\n"; foreach my $coin ( @coins ) { printf("id %s name %s value %d in slot %s\n",$coin->id, $coin->name, $coin->value_cents, $coin->slot_name); } ContentType.pm000444023532023421 231112121654175 16266 0ustar00abrummetgsc000000000000UR-0.41/t/Vendingpackage Vending::ContentType; use strict; use warnings; use Vending; class Vending::ContentType { table_name => 'content_type', id_by => [ type_id => { is => 'integer' }, ], has => [ name => { is => 'varchar' }, machine_id => { value => '1', is_constant => 1, is_classwide => 1, column_name => '' }, machine => { is => 'Vending::Machine', id_by => 'machine_id' }, count => { calculate_from => ['type_id'], calculate => \&count_items_by_type, doc => 'How many items of this type are there' }, ], id_sequence_generator_name => 'URMETA_content_type_TYPE_ID_seq', doc => 'abstract base class for things the machine knows about', schema_name => 'Machine', data_source => 'Vending::DataSource::Machine', }; sub count_items_by_type { my $type_id = shift; my $item = Vending::CoinType->get($type_id) || Vending::Product->get($type_id); my @objects; if ($item->isa('Vending::CoinType')) { @objects = Vending::Coin->get(type_id => $type_id); } else { @objects = Vending::Merchandise->get(product_id => $type_id); } return scalar(@objects); } 1; DataSource000755023532023421 012121654175 15354 5ustar00abrummetgsc000000000000UR-0.41/t/VendingMeta.pm000444023532023421 126112121654172 16732 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourcepackage Vending::DataSource::Meta; use warnings; use strict; use UR; class Vending::DataSource::Meta { is => [ 'UR::DataSource::Meta' ], }; # Don't print out warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::SomeSQLite sub _dont_emit_initializing_messages { my($dsobj, $message) = @_; if ($message =~ m/^Re-creating/) { # don't emit the message about re-creating the DB when run in the test harness $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } 1; Machine.sqlite3-dump000444023532023421 651512121654173 21333 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourceBEGIN TRANSACTION; CREATE TABLE machine (machine_id Integer PRIMARY KEY NOT NULL, address Text); INSERT INTO "machine" VALUES(1,'127.0.0.1'); CREATE TABLE content_type (type_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL); INSERT INTO "content_type" VALUES(1,'dollar'); INSERT INTO "content_type" VALUES(2,'quarter'); INSERT INTO "content_type" VALUES(3,'dime'); INSERT INTO "content_type" VALUES(4,'nickel'); INSERT INTO "content_type" VALUES(9,'shilling'); INSERT INTO "content_type" VALUES(11,'diamond'); CREATE TABLE coin (coin_id integer PRIMARY KEY NOT NULL REFERENCES content(content_id), type_id integer NOT NULL REFERENCES content_type(type_id)); INSERT INTO "coin" VALUES(130,1); INSERT INTO "coin" VALUES(131,1); INSERT INTO "coin" VALUES(132,1); INSERT INTO "coin" VALUES(133,1); INSERT INTO "coin" VALUES(134,1); INSERT INTO "coin" VALUES(135,1); CREATE TABLE product (product_id integer PRIMARY KEY NOT NULL REFERENCES content_type(type_id), cost_cents integer NOT NULL, manufacturer varchar NOT NULL); CREATE TABLE merchandise (merchandise_id integer PRIMARY KEY NOT NULL, product_id integer NOT NULL REFERENCES product(product_id), insert_date datetime NOT NULL DEFAULT (date('now'))); CREATE TABLE URMETA_machine_location_SLOT_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); DELETE FROM sqlite_sequence; INSERT INTO "sqlite_sequence" VALUES('URMETA_machine_location_SLOT_ID_seq',14); INSERT INTO "sqlite_sequence" VALUES('URMETA_content_type_TYPE_ID_seq',11); INSERT INTO "sqlite_sequence" VALUES('URMETA_COIN_COIN_ID_seq',135); CREATE TABLE machine_location (machine_location_id integer PRIMARY KEY NOT NULL, name varchar NOT NULL, is_buyable integer NOT NULL, cost_cents integer, label varchar, machine_id integer NOT NULL REFERENCES machine(machine_id)); INSERT INTO "machine_location" VALUES(8,'a',1,65,'Cookie',1); INSERT INTO "machine_location" VALUES(9,'b',1,100,'Apple',1); INSERT INTO "machine_location" VALUES(10,'c',1,150,'Coke',1); INSERT INTO "machine_location" VALUES(11,'bank',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(12,'box',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(13,'change',0,-1,NULL,1); INSERT INTO "machine_location" VALUES(14,'d',1,10000,'iPod',1); CREATE TABLE content (content_id PRIMARY KEY NOT NULL, subtype_name varchar, machine_location_id integer NOT NULL REFERENCES machine_location(machine_location_id), machine_id NOT NULL REFERENCES machine(machine_id)); INSERT INTO "content" VALUES('129','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('130','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('131','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('132','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('133','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('134','Vending::Coin',12,'1'); INSERT INTO "content" VALUES('135','Vending::Coin',12,'1'); CREATE TABLE URMETA_content_type_TYPE_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); CREATE TABLE URMETA_COIN_COIN_ID_seq (next_value integer PRIMARY KEY AUTOINCREMENT); COMMIT; CoinType.pm000444023532023421 72612121654173 17564 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourcepackage Vending::DataSource::CoinType; use strict; use warnings; use Vending; my $path = Vending->get_base_directory_name() . '/DataSource/coin_types.tsv'; class Vending::DataSource::CoinType { is => ['UR::DataSource::File','UR::Singleton'], has_constant => [ server => { value => $path }, delimiter => { value => '\s+' }, column_order => { value => ['name','value_cents'] }, sort_order => { value => ['name'] }, ], }; 1; coin_types.tsv000444023532023421 6512121654174 20363 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourcediamond 10000 dime 10 dollar 100 nickel 5 quarter 25 Machine.pm000444023532023421 205212121654175 17412 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourcepackage Vending::DataSource::Machine; use strict; use warnings; use Vending; class Vending::DataSource::Machine { is => [ 'UR::DataSource::SQLite', 'UR::Singleton' ], }; use File::Temp; sub server { our $FILE; unless ($FILE) { (undef, $FILE) = File::Temp::tempfile('ur_testsuite_vend_XXXX', OPEN => 0, UNKINK => 0, TMPDIR => 1, SUFFIX => '.sqlite3'); } return $FILE; } # Don't print warnings about loading up the DB if running in the test harness # Similar code exists in URT::DataSource::Meta. sub _dont_emit_initializing_messages { my($dsobj, $msg) = @_; if ($msg =~ m/^Re-creating/) { $_[1] = undef; } } if ($ENV{'HARNESS_ACTIVE'}) { # don't emit messages while running in the test harness __PACKAGE__->warning_messages_callback(\&_dont_emit_initializing_messages); } END { our $FILE; unlink $FILE; } 1; Meta.sqlite3-dump000444023532023421 2263212121654175 20675 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/DataSourceBEGIN TRANSACTION; CREATE TABLE dd_bitmap_index ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, bitmap_index_name varchar NOT NULL, PRIMARY KEY (data_source, owner, table_name, bitmap_index_name) ); CREATE TABLE dd_fk_constraint ( data_source varchar NOT NULL, owner varchar, r_owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, fk_constraint_name varchar NOT NULL, last_object_revision timestamp NOT NULL, PRIMARY KEY(data_source, owner, r_owner, table_name, r_table_name, fk_constraint_name) ); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','MERCHANDISE','PRODUCT','MERCHANDISE_PRODUCT_ID_PRODUCT_PRODUCT_ID_FK','2009-05-07 14:09:28'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','MACHINE_LOCATION','MACHINE','MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK','2009-05-07 14:09:27'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','CONTENT','MACHINE_LOCATION','CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK','2009-05-07 14:09:27'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','CONTENT','MACHINE','CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK','2009-05-07 14:09:29'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','PRODUCT','CONTENT_TYPE','PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK','2009-05-07 14:09:28'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','COIN','CONTENT','COIN_COIN_ID_CONTENT_CONTENT_ID_FK','2009-05-07 14:09:29'); INSERT INTO "dd_fk_constraint" VALUES('Vending::DataSource::Machine','main','','COIN','CONTENT_TYPE','COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK','2009-05-07 14:09:29'); CREATE TABLE dd_fk_constraint_column ( fk_constraint_name varchar NOT NULL, data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, r_table_name varchar NOT NULL, column_name varchar NOT NULL, r_column_name varchar NOT NULL, PRIMARY KEY(data_source, owner, table_name, fk_constraint_name, column_name) ); INSERT INTO "dd_fk_constraint_column" VALUES('COIN_COIN_ID_CONTENT_CONTENT_ID_FK','Vending::DataSource::Machine','main','COIN','CONTENT','COIN_ID','CONTENT_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('COIN_TYPE_ID_CONTENT_TYPE_TYPE_ID_FK','Vending::DataSource::Machine','main','COIN','CONTENT_TYPE','TYPE_ID','TYPE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('MERCHANDISE_PRODUCT_ID_PRODUCT_PRODUCT_ID_FK','Vending::DataSource::Machine','main','MERCHANDISE','PRODUCT','PRODUCT_ID','PRODUCT_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('MACHINE_LOCATION_MACHINE_ID_MACHINE_MACHINE_ID_FK','Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE','MACHINE_ID','MACHINE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('CONTENT_MACHINE_LOCATION_ID_MACHINE_LOCATION_MACHINE_LOCATION_ID_FK','Vending::DataSource::Machine','main','CONTENT','MACHINE_LOCATION','MACHINE_LOCATION_ID','MACHINE_LOCATION_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('CONTENT_MACHINE_ID_MACHINE_MACHINE_ID_FK','Vending::DataSource::Machine','main','CONTENT','MACHINE','MACHINE_ID','MACHINE_ID'); INSERT INTO "dd_fk_constraint_column" VALUES('PRODUCT_PRODUCT_ID_CONTENT_TYPE_TYPE_ID_FK','Vending::DataSource::Machine','main','PRODUCT','CONTENT_TYPE','PRODUCT_ID','TYPE_ID'); CREATE TABLE dd_pk_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, rank integer NOT NULL, PRIMARY KEY (data_source,owner,table_name,column_name,rank) ); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','CONTENT','content_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MACHINE','machine_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','type_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','product_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','COIN','coin_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','merchandise_id',1); INSERT INTO "dd_pk_constraint_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','machine_location_id',1); CREATE TABLE dd_table ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, table_type varchar NOT NULL, er_type varchar NOT NULL, last_ddl_time timestamp, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name) ); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MACHINE','TABLE','entity',NULL,'2009-05-07 14:09:29',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','TABLE','entity',NULL,'2009-05-07 14:09:27',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','TABLE','entity',NULL,'2009-05-07 14:09:30',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','COIN','TABLE','bridge',NULL,'2009-05-07 14:09:29',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','PRODUCT','TABLE','entity',NULL,'2009-05-07 14:09:28',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','CONTENT','TABLE','entity',NULL,'2009-05-07 14:09:30',NULL); INSERT INTO "dd_table" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','TABLE','entity',NULL,'2009-05-07 14:09:29',NULL); CREATE TABLE dd_table_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, column_name varchar NOT NULL, data_type varchar NOT NULL, data_length varchar, nullable varchar NOT NULL, last_object_revision timestamp NOT NULL, remarks varchar, PRIMARY KEY(data_source, owner, table_name, column_name) ); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','PRODUCT_ID','integer',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','NAME','varchar',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','MANUFACTURER','varchar',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','COST_CENTS','integer',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE','MACHINE_ID','Integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','COIN','TYPE_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE_LOCATION_ID','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','CONTENT_ID','',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE','ADDRESS','Text',NULL,'Y','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','IS_BUYABLE','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','LABEL','varchar',NULL,'Y','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT_TYPE','TYPE_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','INSERT_DATE','datetime',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','SUBTYPE_NAME','varchar',NULL,'Y','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','PRODUCT','PRODUCT_ID','integer',NULL,'N','2009-05-07 14:09:28',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','NAME','varchar',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','MACHINE_ID','integer',NULL,'N','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','MACHINE_ID','',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MACHINE_LOCATION','COST_CENTS','integer',NULL,'Y','2009-05-07 14:09:27',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','COIN','COIN_ID','integer',NULL,'N','2009-05-07 14:09:29',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','CONTENT','MACHINE_LOCATION_ID','integer',NULL,'N','2009-05-07 14:09:30',''); INSERT INTO "dd_table_column" VALUES('Vending::DataSource::Machine','main','MERCHANDISE','MERCHANDISE_ID','integer',NULL,'N','2009-05-07 14:09:30',''); CREATE TABLE dd_unique_constraint_column ( data_source varchar NOT NULL, owner varchar, table_name varchar NOT NULL, constraint_name varchar NOT NULL, column_name varchar NOT NULL, PRIMARY KEY (data_source,owner,table_name,constraint_name,column_name) ); COMMIT; Command000755023532023421 012121654175 14700 5ustar00abrummetgsc000000000000UR-0.41/t/VendingNickel.pm000444023532023421 36312121654172 16557 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Nickel; class Vending::Command::Nickel { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'nickel' }, ], doc => 'Insert a Nickel into the machine', }; 1; Dollar.pm000444023532023421 36312121654173 16570 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Dollar; class Vending::Command::Dollar { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'dollar' }, ], doc => 'Insert a dollar into the machine', }; 1; Buy.pm000444023532023421 124112121654173 16126 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Buy; use strict; use warnings; use Vending; class Vending::Command::Buy { is => 'Vending::Command::Outputter', doc => 'Attempt to get a sellable item', has => [ bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ] }; sub help_detail { q(Buy an item from one of the vending machine's slots. Command line argument is one of the slot/button names); } sub _get_items_to_output { my $self = shift; my $slot_names = [$self->bare_args]; my $machine = $self->machine; my @bought = $machine->buy(@$slot_names); return @bought; } 1; Quarter.pm000444023532023421 36712121654174 17003 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Quarter; class Vending::Command::Quarter { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'quarter' }, ], doc => 'Insert a quarter into the machine', }; 1; Dime.pm000444023532023421 35312121654174 16231 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Dime; class Vending::Command::Dime { is => 'Vending::Command::InsertMoney', has => [ name => { is_constant => 1, value => 'dime' }, ], doc => 'Insert a dime into the machine', }; 1; Outputter.pm000444023532023421 66212121654174 17371 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Outputter; use strict; use warnings; class Vending::Command::Outputter { is_abstract => 1, is => 'Vending::Command', doc => 'Abstract parent class for things that output items to the user', }; sub execute { my $self = shift; my @user_items = $self->_get_items_to_output(); foreach my $item ( @user_items ) { print "You get: ",$item->name,"\n"; } return 1; } Menu.pm000444023532023421 142012121654174 16273 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Menu; class Vending::Command::Menu { is => ['UR::Object::Command::List', 'Vending::Command' ], doc => 'Show the items available to buy', has => [ subject_class_name => { is_constant => 1, value => 'Vending::MachineLocation' }, filter => { value => 'is_buyable=1' }, show => { value => 'name,label,price' }, ], }; sub execute { my $self = shift; my $super = $self->super_can('_execute_body'); $super->($self,@_); #$DB::single=1; my $machine = $self->machine; my $inserted = $machine->coin_box->content_value(); if ($inserted) { printf("You have inserted \$%.2f so far\n", $inserted/100); } else { print "You have not inserted any money yet\n"; } return 1; } 1; Service.pm000444023532023421 27512121654175 16757 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::Service; use strict; use warnings; use Vending; class Vending::Command::Service { is => 'Vending::Command', doc => 'Service-mode commands', }; 1; InsertMoney.pm000444023532023421 70612121654175 17632 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::InsertMoney; class Vending::Command::InsertMoney { is => 'Vending::Command', doc => 'Base abstract class for the money inserting commands', #is_abstract => 1, has => [ name => { is => 'String'}, #, is_abstract => 1 }, ] }; sub execute { my $self = shift; my $name = $self->name(); my $machine = $self->machine(); my $worked = $machine->insert($name); return $worked; } 1; CoinReturn.pm000444023532023421 51612121654175 17445 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Commandpackage Vending::Command::CoinReturn; class Vending::Command::CoinReturn { is => 'Vending::Command::Outputter', doc => 'Return all inserted coins back to the customer', }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @items = $machine->coin_return(); return @items; } 1; Service000755023532023421 012121654175 16300 5ustar00abrummetgsc000000000000UR-0.41/t/Vending/CommandConfigureSlot.pm000444023532023421 153012121654172 21552 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Servicepackage Vending::Command::Service::ConfigureSlot; use strict; use warnings; use Vending; class Vending::Command::Service::ConfigureSlot { is => 'Vending::Command::Service', has => [ name => { is => 'String', doc => 'Slot name' }, label => { is => 'String', doc => 'New label for the slot', is_optional => 1 }, cost_cents => { is => 'String', doc => 'New price for this slot', is_optional => 1 }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $loc = $machine->machine_locations(name => $self->name); unless ($loc) { $self->error_message("Not a valid slot name"); return; } if (defined $self->label) { $loc->label($self->label); } if (defined $self->cost_cents) { $loc->cost_cents($self->cost_cents); } return 1; } 1; Show.pm000444023532023421 34612121654173 17674 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Servicepackage Vending::Command::Service::Show; use strict; use warnings; use Vending; class Vending::Command::Service::Show { is => 'Vending::Command::Service', doc => 'Various sub-commands to query the machine state', }; 1; EmptyBank.pm000444023532023421 62612121654174 20650 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Servicepackage Vending::Command::Service::EmptyBank; use strict; use warnings; use Vending; class Vending::Command::Service::EmptyBank { is => ['Vending::Command::Outputter', 'Vending::Command::Service'], doc => 'Get all the money out of the bank', }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @coins = $machine->empty_bank(); return @coins; } 1; RemoveSlot.pm000444023532023421 117512121654175 21076 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Servicepackage Vending::Command::Service::RemoveSlot; use strict; use warnings; use Vending; class Vending::Command::Service::RemoveSlot { is => ['Vending::Command::Outputter', 'Vending::Command::Service'], doc => 'Uninstall the named slot and remove all the items', has => [ name => { is => 'String', doc => 'Name of the slot to empty out' }, ], }; sub _get_items_to_output { my $self = shift; my $machine = $self->machine(); my @items = $machine->empty_machine_location_by_name($self->name); my $loc = $machine->machine_locations(name => $self->name); $loc->delete; return @items; } 1; Add.pm000444023532023421 35312121654175 17444 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Servicepackage Vending::Command::Service::Add; use strict; use warnings; use Vending; class Vending::Command::Service::Add { is => 'Vending::Command::Service', doc => 'Add items to the vending machine', is_abstract => 1, }; 1; Show000755023532023421 012121654175 17220 5ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/ServiceChange.pm000444023532023421 52212121654172 21054 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Change; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Change { is => 'Vending::Command::Service::Show::Money', doc => "Show how much money is in the machine's change dispenser", has => [ location_name => { value => 'change' }, ], }; 1; Inventory.pm000444023532023421 167512121654173 21717 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Inventory; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Inventory { is => [ 'UR::Object::Command::List', 'Vending::Command::Service'], has => [ subject_class_name => { value => 'Vending::Merchandise' }, show => { value => 'id,location_name,name,insert_date' }, filter => { is_calculated => 1 }, bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1 } ], }; sub filter { my $self = shift; my $slot_names = [$self->bare_args]; #$DB::single=1; my $filter = 'machine_id='.$self->machine_id; if (@$slot_names == 1) { $filter = 'slot_name='.$slot_names->[0]; } elsif (@$slot_names) { $filter = 'slot_name=:'.join('/',@$slot_names); } return $filter; } sub execute { #$DB::single = 1; shift->SUPER::_execute_body(@_) } 1; Bank.pm000444023532023421 50012121654174 20540 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Bank; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Bank { is => 'Vending::Command::Service::Show::Money', doc => "Show how much money is in the machine's bank", has => [ location_name => { value => 'bank' }, ], }; 1; Slots.pm000444023532023421 65012121654175 21000 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Slots; use strict; use warnings; use Vending; class Vending::Command::Service::Show::Slots { is => 'UR::Object::Command::List', has => [ subject_class_name => { value => 'Vending::MachineLocation' }, show => { value => 'name,label,price,count,content_value_dollars' }, ], doc => "Display information about what is in the machine's slots", }; 1; Money.pm000444023532023421 166012121654175 21005 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Showpackage Vending::Command::Service::Show::Money; class Vending::Command::Service::Show::Money { is_abstract => 1, is => 'Vending::Command::Service', doc => 'parent class for show change and show bank', has => [ location_name => { is => 'String', is_abstract => 1 }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $loc = $machine->machine_locations(name => $self->location_name); unless ($loc) { $self->error_message("There is no slot named ".$self->location_name); return; } my @coins = $loc->items; my %coins_by_type; my $total_value = 0; foreach my $coin ( @coins ) { $coins_by_type{$coin->name}++; $total_value += $coin->value_cents; } while(my($type,$count) = each %coins_by_type) { printf("%-7s:%6d\n", $type,$count); } printf("Total:\t\$%.2f\n",$total_value/100); return 1; } 1; Add000755023532023421 012121654174 16767 5ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/ServiceChange.pm000444023532023421 176212121654173 20654 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Change; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Change { is => 'Vending::Command::Service::Add', doc => 'Add change to the vending machine', has => [ name => { is => 'String', doc => 'Name of the coin' }, count => { is => 'Integer', doc => 'How many you are adding' }, ], }; sub execute { my $self = shift; my $machine = $self->machine(); my $coin_kind = Vending::CoinType->get(name => $self->name); unless ($coin_kind) { $self->error_message($self->name." is not a valid coin name"); return; } my $change_disp = $machine->change_dispenser; unless ($change_disp) { die "Couldn't retrieve money location for 'change'"; } my $count = $self->count; while($count--) { my $coin = $change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $coin_kind->type_id, machine_id => $self); 1; } return 1; } 1; Slot.pm000444023532023421 151712121654174 20407 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Slot; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Slot { is => 'Vending::Command::Service::Add', doc => 'Install a new vending slot into the machine', has => [ name => { is => 'String', doc => 'Button name for the slot' }, label => { is => 'String', doc => 'Display label for this slot' }, cost => { is => 'Integer', doc => 'Price for this slot, in cents' }, ], }; sub execute { my $self = shift; my $machine = $self->machine; my $slot = $machine->add_machine_location(name => $self->name, label => $self->label, cost_cents => $self->cost, is_buyable => 1); return 1; } 1; Inventory.pm000444023532023421 344312121654174 21463 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/Command/Service/Addpackage Vending::Command::Service::Add::Inventory; use strict; use warnings; use Vending; class Vending::Command::Service::Add::Inventory { is => 'Vending::Command::Service::Add', doc => 'Add a sellable item to the vending machine', has => [ slot => { is => 'String', doc => 'Slot name you are putting the product into' }, name => { is => 'String', doc => 'Name of the item, default is the label on the slot', is_optional => 1 }, count => { is => 'Integer', doc => 'How many you are adding, default is 1', default_value => 1 }, ], }; sub help_detail { q(Add inventory to the machine in the given slot. You are allowed to put items into a slot that do not necessarily match the slot's label name. Example: vend service add inventory --slot a --name Cookie --count 4 ); } sub execute { my $self = shift; my $machine = $self->machine; my $loc = $machine->machine_locations(name => $self->slot); unless ($loc) { die "There is no slot with that name"; } unless (defined $self->name) { print "Adding ",$loc->label,"(s)\n"; $self->name($loc->label); } my $item_kind = $machine->products(name => $self->name); unless ($item_kind) { print "This is a new item. What is the manufacturer:\n"; my $manufacturer = ; print "What is the cost (dollars)\n"; my $price = ; $price = int($price * 100); # Convert to cents $item_kind = $machine->add_product(name => $self->name, manufacturer => $manufacturer, cost_cents => $price); } my $count = $self->count; while($count--) { my $item = $loc->add_item(subtype_name => 'Vending::Merchandise', product_id => $item_kind->id, insert_date => time(), machine_id => $self); } return 1; } 1; t000755023532023421 012121654175 13565 5ustar00abrummetgsc000000000000UR-0.41/t/Vendingbuy_b_not_enough_money.t000444023532023421 301512121654173 20640 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 12; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $prod = Vending::Product->create(name => 'Candy', manufacturer => 'Acme', cost_cents => 100); ok($prod, 'Defined Candy product'); my $slot_b = Vending::MachineLocation->get(name => 'b'); ok($slot_b->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod),'Added Candy to slot a'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('nickel'), 'Inserted a nickel'); my @errors; $machine->dump_error_messages(0); $machine->error_messages_callback(sub { push @errors, $_[1]; }); my @items = $machine->buy('b'); is(scalar(@items), 0, 'Got back no items'); like($errors[0], qr/You did not enter enough money/, 'Error message indicates we did not enter enough money'); @items = $machine->coin_return(); is(scalar(@items), 4, 'Coin return got back 4 items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'quarter'}, 3, 'Three of them were quarters'); is($item_counts{'nickel'}, 1, 'One of them was a nickel'); coin_return.t000444023532023421 125412121654174 16437 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 6; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); my @items = $machine->coin_return(); is(scalar(@items), 2, 'Got back two items'); is($items[0]->name, 'quarter', 'Item 1 is a quarter'); is($items[1]->name, 'quarter', 'Item 2 is a quarter'); buy_a_different_change.t000444023532023421 307412121654174 20544 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 18; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $dime_type = Vending::CoinType->get(name => 'dime'); my $nickel_type = Vending::CoinType->get(name => 'nickel'); # 5 dimes and 5 nickels my $change_disp = $machine->change_dispenser; foreach ( 1 .. 5 ) { ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $nickel_type), 'Added a nickel to the change'); ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $dime_type), 'Added a dime to the change'); } my $prod = Vending::Product->create(name => 'Orange', manufacturer => 'Acme', cost_cents => 65); ok($prod, 'Defined "Orange" product'); my $slot_a = $machine->machine_locations(name => 'a'); my $inv = $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($inv, 'Added an orange to slot A'); ok($machine->insert('dollar'), 'Inserted a dollar'); my @items = $machine->buy('a'); is(scalar(@items), 5, 'Got back five items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'Orange'}, 1, 'One of them was an Orange'); is($item_counts{'nickel'}, 1, 'One of them was a nickel'); is($item_counts{'dime'}, 3, 'Three of them were dimes'); buy_a_not_enough_change.t000444023532023421 401512121654174 20737 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 16; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine, but not enough change my $quarter_type = Vending::CoinType->get(name => 'quarter'); my $change_disp = $machine->change_dispenser; ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $quarter_type),'Added a quarter to the change'); my $prod = Vending::Product->create(name => 'Orange', manufacturer => 'Acme', cost_cents => 65); ok($prod, "Defined 'Orange' product"); my $slot_a = Vending::MachineLocation->get(name => 'a'); my $inv = $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($inv, 'Added an orange to slot A'); ok($machine->insert('dollar'), 'Inserted a dollar'); my @errors; $machine->dump_error_messages(0); $machine->error_messages_callback(sub { push @errors, $_[1]; }); my @items = $machine->buy('a'); is(scalar(@items), 0, 'Got no items'); like($errors[0], qr(Not enough change), 'Error message indicated not enough change'); @items = $machine->coin_return(); is(scalar(@items),1, 'Coin return got us one thing back'); is($items[0]->name, 'dollar', 'The returned thing was a dollar'); is($items[0]->value, 100, 'The returned thing was worth 100 cents'); # Poke the machine and make sure everything is still in there @items = Vending::Merchandise->get(); is(scalar(@items), 1, 'There is one item still in the inventory'); is($items[0]->name, 'Orange', 'It was an Orange'); is($items[0]->machine_location, $slot_a, 'The orange is in slot a'); my $bank = $machine->bank(); @items = $bank->items(); is(scalar(@items), 0, 'Nothing in the bank'); @items = $change_disp->items(); is(scalar(@items), 1, 'One thing in the change dispenser'); is($items[0]->name, 'quarter', 'It is a quarter'); buy_b_with_exact_change.t000444023532023421 220312121654174 20727 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 10; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $prod = Vending::Product->create(name => 'Apple', manufacturer => 'Acme', cost_cents => 100); ok($prod, 'Created a product type Apple'); my $slot = Vending::MachineLocation->get(name => 'b'); ok($slot, 'Got object for slot b'); my $item = $slot->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($item, 'Added an Apple inventory item to slot b'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); ok($machine->insert('quarter'), 'Inserted a quarter'); my @items = $machine->buy('b'); is(scalar(@items), 1, 'Got back one item'); is($items[0]->name, 'Apple', 'It was an Apple'); buy_a_get_change_back.t000444023532023421 273212121654175 20336 0ustar00abrummetgsc000000000000UR-0.41/t/Vending/tuse strict; use warnings; use Test::More tests => 9; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../.."; # For the Vending namespace use lib File::Basename::dirname(__FILE__)."/../../../.."; # For the UR namespace use Vending; my $machine = Vending::Machine->get(); ok($machine, 'Got the Vending::Machine instance'); $machine->_initialize_for_tests(); # Stock the machine so there's something to get my $quarter_type = Vending::CoinType->get(name => 'quarter'); my $dime_type = Vending::CoinType->get(name => 'dime'); my $change_disp = $machine->change_dispenser; ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $quarter_type), "Added a quarter to the change"); ok($change_disp->add_item(subtype_name => 'Vending::Coin', type_id => $dime_type), "Added a dime to the change"); my $prod = Vending::Product->create(name => 'Battery', manufacturer => 'Acme', cost_cents => 65); ok($prod, "defined Battery product"); my $slot_a = Vending::MachineLocation->get(name => 'a'); $slot_a->add_item(subtype_name => 'Vending::Merchandise', product_id => $prod); ok($machine->insert('dollar'), 'Inserted a dollar'); my @items = $machine->buy('a'); is(scalar(@items), 3, 'Got back three items'); my %item_counts; foreach my $item ( @items ) { $item_counts{$item->name}++; } is($item_counts{'Battery'}, 1, 'One of them was a Battery'); is($item_counts{'quarter'}, 1, 'One of them was a quarter'); is($item_counts{'dime'}, 1, 'One of them was a dime'); CdExample000755023532023421 012121654175 13572 5ustar00abrummetgsc000000000000UR-0.41/tArtist.pm000444023532023421 64412121654173 15515 0ustar00abrummetgsc000000000000UR-0.41/t/CdExample package CdExample::Artist; use CdExample; class CdExample::Artist { id_by => 'artist_id', has => [ name => { is => 'Text' }, cds => { is => 'CdExample::Cd', is_many => 1, reverse_as => 'artist' }, foo => { is => 'Text' }, #bar => { is => 'Text' }, baz => { is => 'Text' }, ], data_source => 'CdExample::DataSource::DB1', table_name => 'ARTISTS', }; 1; Cd.pm000444023532023421 61412121654175 14574 0ustar00abrummetgsc000000000000UR-0.41/t/CdExamplepackage CdExample::Cd; use CdExample; class CdExample::Cd { id_by => 'cd_id', has => [ artist => { is => 'CdExample::Artist', id_by => 'artist_id' }, title => { is => 'Text' }, year => { is => 'Integer' }, artist_name => { via => 'artist', to => 'name' }, ], data_source => 'CdExample::DataSource::DB1', table_name => 'CDS', }; 1; CmdTest000755023532023421 012121654175 13273 5ustar00abrummetgsc000000000000UR-0.41/tC1.pm000555023532023421 212312121654174 14231 0ustar00abrummetgsc000000000000UR-0.41/t/CmdTestuse Command::V2; use strict; use warnings; package CmdTest::C1; use CmdTest::Stuff; class CmdTest::C1 { is => 'Command::V2', has_optional_input => [ z => { is => "Text" }, a => { is => "Text" }, b20 => { is => "Text" }, b3 => { is => "Text" }, ], has_optional_param => [ p3 => { is => 'Number' }, p1 => { is => 'Number' }, p2 => { is => 'Number' }, ], has_input => [ rz => { is => "Text" }, ra => { is => "Text" }, rb20 => { is => "Text" }, rb3 => { is => "Text" }, ], has_param => [ rp3 => { is => 'Number' }, rp1 => { is => 'Number' }, rp2 => { is => 'Number' }, ], has_output => [ #stuff => { is => 'CmdTest::Stuff' }, more => { is => 'Text' } ], doc => "test command 1" }; sub execute { my $self = shift; print "running $self with args: " . Data::Dumper::Dumper($self) . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; Stuff.pm000444023532023421 15412121654174 15034 0ustar00abrummetgsc000000000000UR-0.41/t/CmdTestpackage CmdTest::Stuff; class CmdTest::Stuff { has => [ foo => { is => "Text" }, ] }; 1; C3.pm000555023532023421 75312121654174 14222 0ustar00abrummetgsc000000000000UR-0.41/t/CmdTestpackage CmdTest::C3; use Command::V2; use strict; use warnings; use CmdTest::C2; class CmdTest::C3 { is => ['CmdTest::C2'], has => [ thing_name => { is => 'Text', via => 'thing', to => 'name' }, ], doc => "test command 3" }; sub execute { my $self = shift; no warnings; print "thing_id is " . $self->thing_id . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; C2.pm000555023532023421 131512121654175 14235 0ustar00abrummetgsc000000000000UR-0.41/t/CmdTestuse Command::V2; use strict; use warnings; package CmdTest::C2; use CmdTest::Stuff; class CmdTest::Thing { has => [ name => { is => 'Text' } ] }; CmdTest::Thing->create(id => 111, name => 'one'); CmdTest::Thing->create(id => 222, name => 'two'); CmdTest::Thing->create(id => 333, name => 'three'); class CmdTest::C2 { is => 'Command::V2', has => [ thing => { is => 'CmdTest::Thing', id_by => 'thing_id' }, ], doc => "test command 2" }; sub execute { my $self = shift; print "running $self with args: " . Data::Dumper::Dumper($self) . "\n"; return 1; } if ($0 eq __FILE__) { exit __PACKAGE__->_cmdline_run(@ARGV) } sub help_detail { return "HELP DETAIL"; } 1; t000755023532023421 012121654175 13536 5ustar00abrummetgsc000000000000UR-0.41/t/CmdTest01-mutual-resolution-via-to.t000444023532023421 167512121654175 21214 0ustar00abrummetgsc000000000000UR-0.41/t/CmdTest/t#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 5; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../../lib"; use lib File::Basename::dirname(__FILE__)."/../.."; use UR; use Command::Shell; use CmdTest; use CmdTest::C2; use CmdTest::C3; # Put this into Perl5Lib so when we exec the commands below, they can # find CmdTest::Stuff $ENV{PERL5LIB} .= ':' . File::Basename::dirname(__FILE__)."/../.."; ok(CmdTest->isa('Command::Tree'), "CmdTest isa Command::Tree"); use_ok("CmdTest::C3"); my $path = $INC{"CmdTest/C3.pm"}; ok($path, "found path to test module") or die "cannot continue!"; my $result1 = `$^X $path --thing=two`; chomp $result1; is($result1, "thing_id is 222", "specifying an object automatically specifies its indirect value"); my $result2 = `$^X $path --thing-name=two`; chomp $result2; is($result2, "thing_id is 222", "specifying an indirect value automatically sets the value it is via"); newnamespace000755023532023421 012121654175 14376 5ustar00abrummetgsc000000000000UR-0.41/t01_command_define_namespace.t000444023532023421 355712121654175 22176 0ustar00abrummetgsc000000000000UR-0.41/t/newnamespace#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 14; use File::Basename; use lib File::Basename::dirname(__FILE__)."/../../lib"; use File::Temp; use Cwd; use UR; my $current_dir = Cwd::cwd; END { chdir $current_dir }; # so the temp dir can get cleaned up my $tempdir = File::Temp::tempdir(CLEANUP => 1); ok($tempdir, 'make temp dir'); chdir($tempdir); push @INC,$tempdir; # so it can find the namespace modules it's creating my $cmd = UR::Namespace::Command::Define::Namespace->create(nsname => 'NewNamespace'); ok($cmd, 'create UR::Namespace::Command::Define::Namespace'); $cmd->dump_status_messages(0); $cmd->dump_error_messages(0); $cmd->dump_warning_messages(0); $cmd->queue_status_messages(1); $cmd->queue_error_messages(1); $cmd->queue_warning_messages(1); ok($cmd->execute, 'execute'); my $namespace = UR::Namespace->get('NewNamespace'); ok($namespace, 'Namespace object created'); my $data_source = UR::DataSource->get('NewNamespace::DataSource::Meta'); ok($data_source, 'Metadata data source object created'); ok(-f 'NewNamespace.pm', 'NewNamespace.pm module exists'); ok(-d 'NewNamespace', 'NewNamespace directory exists'); ok(-d 'NewNamespace/DataSource', 'NewNamespace/DataSource directory exists'); ok(-f 'NewNamespace/DataSource/Meta.pm', 'NewNamespace/DataSource/Meta.pm module exists'); ok(-f 'NewNamespace/Vocabulary.pm', 'NewNamespace/Vocabulary.pm module exists'); my @messages = $cmd->status_messages(); is($messages[0], 'A NewNamespace (UR::Namespace)', 'Message adding NewNamespace'); is($messages[1], 'A NewNamespace::Vocabulary (UR::Vocabulary)', 'Message adding vocabulary'); is($messages[2], 'A NewNamespace::DataSource::Meta (UR::DataSource::Meta)', 'Message adding meta datasource'); like($messages[3], qr(A /.+/NewNamespace/DataSource/Meta\.sqlite3n?-dump [(]Metadata DB skeleton[)]), 'Message adding metaDB dump file'); gmt-web000755023532023421 012121654174 13026 5ustar00abrummetgsc000000000000UR-0.41common.yml000444023532023421 63312121654174 15160 0ustar00abrummetgsc000000000000UR-0.41/gmt-web--- layout: sub menu: - text: Overview href: index.html - text: Documentation href: documentation.html - text: Install href: install.html module_details: name: 'UR' exe_name: ur gmt_pkg_name: ur debian_pkg_name: libur-perl icon: res/images/icon_48.png icon_16: res/images/icon_16.png download_link: install.html github_url: https://github.com/genome/UR content000755023532023421 012121654175 14501 5ustar00abrummetgsc000000000000UR-0.41/gmt-webdocumentation.html000444023532023421 2735412121654173 20426 0ustar00abrummetgsc000000000000UR-0.41/gmt-web/content

UR User Manual



NAME

UR::Manual::Overview - UR from Ten Thousand Feet


Perspective on Objects

Standard software languages provide a facility for making objects. Those objects have certain characteristics which are different with UR objects.

A standard object in most languages:

  • exists only as long as the program which created it has a reference to it
  • requires that the developer manage organizing the object(s) into a structure to support any searching required
  • handles persistence between processes explicitly, by saving or loading the object to external storage
  • references other objects only if explicitly linked to those objects
  • acts as a functional software device, but any meaning associated with the object is implied by how it is used

Regular objects like those described above are the building blocks of most software.

In many cases, however, they are often used for a second, higher-level purpose: defining entities in the domain model of the problem area the software addresses. UR objects are tailored to represent domain model entities well. In some sense, UR objects follow many of the design principles present in relational databases, and as such mapping to a database for UR objects is trivial, and can be done in complex ways.

UR objects differ from a standard object in the following key ways:

  • the object exists after creation until explicitly deleted, or the transaction it is in rolled-back
  • managing loaded objects is done automatically by a Context object, which handles queries, saving, lazy-loading and caching
  • it is possible to query for an object by specifying the class and the matching characteristics
  • the object can reference other objects which are not loaded in the current process, and be referenced by objects not in the current process
  • the object is a particular truth-assertion in the context in which it exists

Object-Relational Mapping

UR's primary reason for existing is to function as an ORM. That is, managing how to store instances of objects in memory of a running program with more persistant storage in a relational database, and retrieve them later. It handles the common cases where each table is implemented by a class their columns are properties of the classes; retrieving objects by arbitrary properties; creating, updating and deleting objects with enforced database constraints; and named relationships between classes.

It can also handle more complicated things like:

  • classes for things which are not database entities at all
  • derived classes where the data spans multiple tables between the parent and child classes
  • loading an object through a parent class and having it automatically reblessed into the appropriate subclass
  • properties with no DB column behind them
  • calculated properties with a formula behind them
  • inheritance hierarchies that may have tables missing at some or all stages
  • meta-data about Properties, Classes and the relationships between them

Object Context

With UR, every object you create is made a part of the current ``Context''. Conceptually, the Context is the lens by which your application views the data that exists in the world. At one level, you can think of the current context as an in-memory transaction. All changes to the object are tracked by the context. The Context knows how to map objects to their storage locations, called Data Sources. Saving your changes is simply a matter of asking the current context to commit.

The Context can also reverse the saving process, and map a request for an object to a query of external storage. Requests for objects go through the Context, are loaded from outside as needed, and are returned to the caller after being made part of the current context's transaction.

Objects never reference each other by actual Perl reference internally, instead they use the referent's ID. Accessors on an object which return another object send the ID through the context to get the object back, allowing the context to load the referenced object only when it is actually needed. This means that your objects can hook together until references span an entire database schema, and pulling one object from the database will not load the entire database into memory.

The context handles caching, and by default will cache everything it touches. This means that you can ask for the same thing multiple times, and only the first request will actually hit the underlying database. It also means that requests for objects which map to the same ID will return the exact same instance of the object.

The net effect is that each process's context is an in-memory database. All object creation, deletion, and change is occurring directly to that database. For objects configured to have external persistence, this database manages itself as a ``diff'' vs. the external database, allowing it to simulate representing all UR data everywhere, while only actually tracking what is needed.

Benefits

  • database queries don't repeat themselves again and again
  • you never write insert/update/delete statements, or work out constraint order yourself
  • allows you to write methods which address an object individually, with ways to avoid many individual database queries
  • explicitly clearing the cache is less complex than explicitly managing the caching of data

Issues

  • the cache grows until you explicitly clear it, or allow the Context to prune the cache by setting object count limits explicitly
  • there is CPU overhead checking the cache if you really are always going directly to the database

Class Definitions

At the top of every module implementing a UR class is a block of code that defines the class to explicitly spell out its inheritance, properties and types, constraints, relationships to other classes and where the persistent storage is located. It's meant to be easy to read and edit, if necessary. If the class is backed by a database table, then it can also maintain itself.


Metadata

Besides the object instances representing data used by the program, the UR system has other objects representing metadata about the classes (class information, properties, relationships, etc), database entities (databases, tables, columns, constraints, etc), transactions, data sources, etc. All the metadata is accessable through the same API as any of the database-backed data.

For classes backed by the database, after a schema change (like adding tables or columns, altering types or constraints), a command-line tool can automatically detect the change and alter the class definition in the Perl module to keep the metadata in sync with the database.


Documentation System

At the simplest level, most entities have a 'doc' metadata attribute to attach some kind of documentation to. There's also a set of tools that can be run from the command line or a web browser to view the documentation. It can also be used to browse through the class and database metadata, and generate diagrams about the metadata.


Iterators

If a retrieval from the database is likely to result in the generation of tons of objects, you can choose to get them back in a list and keep them all in memory, or get back a special Iterator object that the program can use to get back objects in batches.


Command Line Tools

UR has a central command-line tool that cam be used to manipulate the metadata in different ways. Setting up namespaces, creating data sources, syncing classes with schemas, accessing documentation, etc.

There is also a framework for creating classes that represent command line tools, their parameters and results, and makes it easy to create tools through the Command Pattern.


Example

Given these classes:

PathThing/Path.pm

  use strict;
  use warnings;

  use PathThing;  # The application's UR::Namespace module

  class PathThing::Path {
      id_by => 'path_id',
      has => [
          desc   => { is => 'String' },
          length => { is => 'Integer' },
      ],
      data_source => 'PathThing::DataSource::TheDB',
      table_name => 'PATHS',
  };

PathThing/Node.pm

  class PathThing::Node {
      id_by => 'node_id',
      has => [
          left_path => { is => 'PathThing::Path', id_by => 'left_path_id' },
          left_path_desc => { via => 'left_path', to => 'desc' },
          left_path_length => { via => 'left_path', to => 'length' },
          right_path => { is => 'PathThing::Path', id_by => 'right_path_id' },
          right_path_desc => { via => 'right_path', to => 'desc' },
          right_path_length => { via => 'right_path', to => 'length' },
          map_coord_x => { is => 'Integer' },
          map_coord_y => { is => 'String' },
      ],
      data_source => 'PathThing::DataSource::TheDB',
      table_name => 'NODES',
  };

For a script like this one:

  use PathThing::Node;
  my @results = PathThing::Node->get(
                    right_path_desc => 'over the river',
                    left_path_desc => 'through the woods',
                    right_path_length => 10,
                );

It will generate SQL like this:

  select NODES.NODE_ID, NODES.LEFT_PATH_ID, NODES.RIGHT_PATH_ID,
         NODES.MAP_COORD_X, NODES.MAP_COORD_Y,
         left_path_1.PATH_ID, left_path_1.DESC, left_path_1.LENGTH
         right_path_1.PATH_ID, right_path_1.DESC, right_path_1.LENGTH
  from NODES
  join PATHS left_path_1 on NODES.LEFT_PATH_ID = left_path_1.PATH_ID
  join PATHS right_path_1 on NODES.RIGHT_PATH_ID = right_path1.PATH_ID
  where left_path_1.DESC = 'through the woods'
    and right_path_1.DESC = 'over the river',
    and right_path_1.LENGTH = 10

And for every row returned by the query, a PathThing::Node and two PathThing::Path objects will be instantiated and stored in the Context's cache. @results will contain a list of matching PathThing::Node objects.

install.md000444023532023421 33512121654174 16606 0ustar00abrummetgsc000000000000UR-0.41/gmt-web/content{% include install/download.html %} It is also availabe from [CPAN](http://search.cpan.org/search?mode=all&query=UR). {% include install/github.html %} {% include install/help.html %} {% include install/manuals.html %} index.html000444023532023421 271612121654175 16641 0ustar00abrummetgsc000000000000UR-0.41/gmt-web/content

Introduction


UR is a Class Framework and Object/Relational Mapper (ORM) for Perl.

After installing, run the "ur" command for a list of options.

As a Class Framework, it starts with the familiar Perl meme of the blessed hash reference as the basis for object instances, and builds upon that with a more formal way to describe classes and their properties, object caching, and metadata about the classes and the ways they connect to each other.

As an ORM, it aims to relieve the developer from having to think about the SQL behind any particular request, instead using the class structure and its metadata as a guide for where the data will be found. Behind the scenes, the RDBMS portion can handle JOINs (both INNER and OUTER) representing inheritance and indirect properties, multi-column primary and foreign keys, and iterators. It does its best to only query the database for information you've directly asked for, and to not query the database for something that has been loaded before. Oracle, SQLite, MySQL and PostgreSQL are all supported.

Additionally, UR can use files or collections of files as if they were tables in a database, as well as internally handling the equivalent of an SQL join between two or more databases if that's what the query and class structure indicates.

UR.pm contains more introductory POD documentation. UR::Manual has a short list of documentation you're likely to want to see next.

res000755023532023421 012121654173 15270 5ustar00abrummetgsc000000000000UR-0.41/gmt-web/contentimages000755023532023421 012121654174 16536 5ustar00abrummetgsc000000000000UR-0.41/gmt-web/content/resicon_48.png000444023532023421 543012121654173 20645 0ustar00abrummetgsc000000000000UR-0.41/gmt-web/content/res/imagesPNG  IHDR00`ntEXtSoftwareAdobe ImageReadyqe< IDATxڬYkoT>3wcǦ$(uqk7\$B JP &"I2ZU6R$JZQL X upk Q3.kC scv|M^G+}˗SM'Ù.fQҶ@#49X gfkj 42凌88cXpͣq-1X# k@@ncYYy*tEyyiMMȗs7E'T ךN 5I_}І lz-!EOSJɲք 2 o4$A[ӊ To_,XYխ=`m|r(*[VѬ œQ1b蛶kodXQk/a&Ґ+*`4֫^9??!qZ$[O—҉_}]Ʊd-}}~}vĀ޹yP7?0J&K:;}! (*FzROohAb!3=wٵxxE{fQg DeOĉ?=[7M}{ kdIbn।Q+0(4è&|-֭Kww01@ʵL9M6 H!9RĝȐ(!٘atvL&%~h``y^'d^0ң&SɅeן$imL^*GUOw.S\?r\Y>$jt'.Ned8f.,&!~a6Bzaa10_Q) T"Ҹ:1\ȉ7pܶm;ɷ%R  "RHʪD)&ӧ7ДQ"$4l1GQLxsGB2*HT ر76oٲe5ͰP+Fk  !p BSH@ ̪5Dщb%Ðx]|>܆[_A%c(CF3TI5E1f}6QP" T,ݾ=z/¼IEQK4Ձa,ZMI uŴTR ӳ.D>c~;EC( qr=8{?1'fxBs^ ADt"gKsvDN6xTрz3&]QCL&^ iIc* +1b1Vt="xYJT.VUUs; tKu\#pnNMM,I9*/XUڄH ]ʤMy1tITt 7-.{GʯԼzj1LWejtӇMRD a#MM%Yݺu0c&Plx:רb]7x_K퍬֞=xs;;vZ+H0#uMMmO8̏ް 67"K T?P8~Ç֮ḿ;ҧm۶ڵKC} Hgcs"d48_L_li40q~t6??X͕+j`$wEqX mdIENDB`icon_16.png000444023532023421 127412121654174 20643 0ustar00abrummetgsc000000000000UR-0.41/gmt-web/content/res/imagesPNG  IHDRh6tEXtSoftwareAdobe ImageReadyqe<^IDATxdRAOAޙ]VhL[MH0V!` iLp 'p=6)7¥mꁦ!F!`imKٝݣyo7@o!|}SUcRWWRLpE<5-<}vtcNh!Vж''zn4`"hd29AW&4MK&xsq< д %_[[[OF^*u4lV>?ϘfYX<Hrw}bBaqfƆLve/5 5SZ*}]XX[x@2\Á`i9wvv>#a^qz`0bP=<vquVnv,˲GHNt.`NO?H${zzl6/~G! <+á bnn~|<%Qόor0l4 J\WP̀FۢQ 'UR::Namespace::Command', above => 1;"; exit if ($@); } if ($ENV{COMP_LINE}) { #for transitioning from older version of completion #just return no result exit; } }; use above "UR"; UR::Namespace::Command->execute_with_shell_params_and_exit(); =pod =head1 NAME B - command-line interface to UR =head1 DESCRIPTION The B command is the entry point for a suite of tools to create and manage a module tree of UR classes, data sources, and views. It also includes launchers for some built-in services. =head1 SUB-COMMANDS See the help on specific sub-commands for details. init NAMESPACE [DB] initialize a new UR app in one command define ... define namespaces, data sources and classes describe CLASSES-OR-MODULES show class properties, relationships, meta-data update ... update parts of the source tree of a UR namespace list ... list objects, classes, modules sys ... service launchers test ... tools for testing and debugging =head1 DEVELOPMENT =head2 PWD Running this WITHIN the source tree of a UR namespace will automatically "use lib" your tree. A message will appear to STDERR when this occurs. See the module for context-sensitive library usage info. =head2 MAC SOFTWARE MODULE API Looking for the docs on UR.pm on a Mac? Try "perldoc UR.pm" or "man UR". On some systems (Mac), perldoc will show this page for both "perldoc ur" and also "perldoc UR" due to filesystem case insensitivity. =head2 SOURCE UR is hosted on github, at: http://github.com/sakoht/ur =head1 BUGS Report bugs at http:://github.com/sakoht/ur/issues =head1 AUTHOR Scott Smith (sakoht) at cpan.org =cut