diff --git a/.golangci.yml b/.golangci.yml index f0a3d2c..0b46d18 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,6 +9,7 @@ linters: - depguard - dupl # cobra - gofumpt + - err113 - exhaustruct - gochecknoinits # cobra - gochecknoglobals # cobra @@ -18,8 +19,6 @@ linters: - varnamelen - forbidigo # todo - - wrapcheck # todo - - err113 # todo - tenv # deprecated @@ -27,18 +26,15 @@ linters: linters-settings: revive: rules: - - name: var-naming # todo - disabled: true - - stylecheck: - checks: - - "-ST1003" # todo + - name: unused-parameter + arguments: + - allowRegex: "cmd||args" issues: exclude-files: - extract.* + - constant/func.go - cmd/dbgo_gen/run.go - - cmd/dbgo_query/template_merger.go - - cmd/dbgo_query/template_jet.go - - \ No newline at end of file + - cmd/dbgo_query/schema_merger.go + - cmd/dbgo_query/schema_jet.go + - cmd/dbgo_query/schema_xstruct.go diff --git a/LICENSE b/LICENSE index 84ed6d0..29ebfa5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -MIT License - -Copyright (c) 2025 SwitchUpCB - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..12d7ef7 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,9 @@ +Copyright (C) 2025 SwitchUpCB + +This file is part of dbgo. + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. diff --git a/README.md b/README.md index 617db6d..16db9d2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ _NOTE: You can read the [roadmap](/ROADMAP.md) for a list of implemented feature ## Why don't you use other database frameworks? -`dbgo` gives you the option to use domain models as a source of truth for optimized code, while other database frameworks generate unoptimized code based on the database as a source of truth. +`dbgo` lets you use domain models as a source of truth for optimized code, while other database frameworks generate unoptimized code based on the database as a source of truth. **Here is an example of the difference between `dbgo` and other frameworks.** @@ -136,43 +136,42 @@ custom: Use the `dbgo query` manager to save customized type-safe SQL statements or generate them. -**1\)** Install the command line tool: `xstruct`. -``` -go install github.com/switchupcb/xstruct@latest -``` - -**2\)** Install the command line tool: `sqlc`. +**1\)** Install the command line tool: `sqlc`. ``` go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest ``` -**3\)** Install the command line tool: `dbgo`. +**2\)** Install the command line tool: `dbgo`. ``` go install github.com/switchupcb/dbgo@latest ``` -**4\)** Run the executable with the following options to add SQL to the queries directory. +**3\)** Run the executable with the following options to add SQL to the queries directory. | Command Line | Description | | :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `db query schema -y path/to/yml` | Generates a `schema.sql` and `schema.go` file representing your database in the queries directory. | | `db query gen -y path/to/yml` | Generates SQL queries for Read (Select) operations and Create (Insert), Update, Delete operations. | | `db query template -y path/to/yml` | Adds a `name` template to the queries `templates` directory. The template contains Go type database models you can use to return a type-safe SQL statement from the `SQL()` function in `name.go` which is called by `db query save`. | | `db query save -y path/to/yml` | Saves an SQL file _(with the same name as the template \[e.g., `name.sql`\])_ containing an SQL statement _(returned from the `SQL()` function in `name.go`)_ to the queries directory. | -_Here are additional usage notes._ +_Here are additional command usage notes._ + - _`-y`, `--yml`: The path to the YML file must be specified in reference to the current working directory._ - _`db query template`: Every template is updated when this command is executed without a specified template._ - _`db query save`: Every template is saved when this command is executed without a specified template._ -- _`db query save`: You are not required to initialize a `go.mod` file to run templates, but using `go get github.com/switchupcb/jet/v2@dbgo` in a `go.mod` related to the template files helps you identify compiler errors in your template files._ #### How do you develop type-safe SQL? -Running `db query template -y path/to/yml` adds a `name.go` file with database models as Go types to your queries directory. +Running `db query template -y path/to/yml` adds a `name.go` file with database models as Go types to your queries directory: You can use these Go types with [`jet`](https://github.com/go-jet/jet) to return an `stmt.Sql()` from `SQL()`, which cannot be interpreted unless the Go code referencing struct fields can be compiled. -Use these Go types with [`jet`](https://github.com/go-jet/jet) to return an `stmt.Sql()` from `SQL()`, which cannot be interpreted unless the Go code referencing struct fields can be compiled. +_Read "How quickly bugs are found" for more information about writing type-safe SQL with Go._ -_Read "How quickly bugs are found" for more information._ +You should consider these interpreter usage notes while using templates. +- You do not have to use `jet` to generate SQL programmatically. +- You are not required to initialize a `go.mod` file to run templates, but using `go get github.com/switchupcb/jet/v2@dbgo` in a `go.mod` related to the template files helps you identify compiler errors in your template files while using `jet`. +- `db query save ` interprets `schema.go` before `name.go`. So, do not reference declarations from `name.go` in `schema.go`. ### Step 6. Generate the database consumer package @@ -194,11 +193,18 @@ _The path to the YML file must be specified in reference to the current working ## What is the License? -`dbgo` uses an [MIT License](https://opensource.org/license/mit). +`dbgo` uses a [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.en.html). + ### What can you do with this license? -Code generated by `dbgo` can be used without restriction (including proprietary and commercial usage). However, modifications to the `dbgo` Software Source Code or implementing `dbgo` in a larger work programmatically requires you to "include the copyright notice and permission notice in the license in all copies or substantial portions of the Software". +Code generated by `dbgo` can be used without restriction (including proprietary and commercial usage). However, modifications to the `dbgo` Software Source Code or implementing `dbgo` in a larger work programmatically requires you to to [adhere to the AGPLv3 License](https://www.gnu.org/licenses/gpl-faq.html). + +### What is a license exception? + +A license exception lets you modify and use `dbgo` **without restriction**. + +You can receive a license exception for `dbgo` by contacting SwitchUpCB using the [`dbgo` License Exception Inquiry Form](https://switchupcb.com/dbgo-license-exception/). ## Contributing diff --git a/ROADMAP.md b/ROADMAP.md index 2859f30..ee54682 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,19 +4,23 @@ Here is the feature release schedule for this software. ## Implemented +You can use `dbgo` with a PostreSQL database. + You can use the `dbgo query` manager to manage your SQL statements or generate them. - `dbgo query gen` - `dbgo query template` - `dbgo query save` - -## March 10, 2025: `dbgo gen` (without domain) - + You can use `dbgo gen` to generate Database Driver Go code which calls your SQL queries based on the database as a single source of truth. ## March 17, 2025: `dbgo gen` (with domain) You can use `dbgo gen` to generate Database Driver Go code which calls your SQL queries based on the domain as a single source of truth. +You can place a `_dbgo.sql` file in your queries directory to generate Go code without an SQL file combination operation when `dbgo gen` is called. + +You can use options to only generate a `combined.SQL` file (currently output from the implemented `--keep` option) or only generate Database Driver Go code (from the `_dbgo.sql` file). + ## March 24, 2025: `dbgo gen` (with .go templates) You can use `dbgo gen` with `.go` files to customize the code generation algorithm, which will be updated to — by default — generate SQL statement-calling Go code for @@ -33,4 +37,12 @@ Structural optimizations provided by the program are already implemented by Marc ## Future: Stored Procedures -You can use `dbgo` to add Stored Procedures to your database for Create (Insert), Read, and Update operations. \ No newline at end of file +You can use `dbgo` to add Stored Procedures to your database for Create (Insert), Read, and Update operations. + +## Future: Automatic sqlc Query Annotation Developer + +You can use a customizable [sqlc Query Annotation](https://docs.sqlc.dev/en/stable/reference/query-annotations.html) developer to automatically add query annotations to your `dbgo gen` SQL file before Go code generation occurs. + +## Future: Database Support + +You can use `dbgo` with SQLITE3 and MySQL. diff --git a/cmd/config/yml.go b/cmd/config/yml.go index e615024..e8c2ea1 100644 --- a/cmd/config/yml.go +++ b/cmd/config/yml.go @@ -13,18 +13,18 @@ import ( func LoadYML(relativepath string) (*YML, error) { file, err := os.ReadFile(relativepath) if err != nil { - return nil, fmt.Errorf("the specified .yml filepath doesn't exist: %v\n%w", relativepath, err) + return nil, fmt.Errorf("yml read: %w", err) } yml := new(YML) if err := yaml.Unmarshal(file, yml); err != nil { - return nil, fmt.Errorf("error occurred unmarshalling the .yml file\n%w", err) + return nil, fmt.Errorf("yml unmarshal: %w", err) } // determine the actual filepath of the setup file. absloadpath, err := filepath.Abs(relativepath) if err != nil { - return nil, fmt.Errorf("error occurred while determining the absolute file path of the setup file\n%v", relativepath) + return nil, fmt.Errorf("error determining the absolute file path of the setup file: %q\n%w", relativepath, err) } yml.abspath = absloadpath diff --git a/cmd/constant.go b/cmd/constant.go deleted file mode 100644 index 5e038e3..0000000 --- a/cmd/constant.go +++ /dev/null @@ -1,53 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "path/filepath" - - "github.com/switchupcb/dbgo/cmd/config" -) - -const ( - flag_yml_name = "yml" - flag_yml_shorthand = "y" - flag_yml_usage = `The path to the .yml flag used for code generation (from the current working directory)` - flag_yml_usage_unspecified = "you must specify a .yml configuration file using --yml path/to/yml" - - queriesGoTemplatesDirname = "templates" -) - -var ( - ymlFlag = new(string) -) - -// parseFlagYML parses a "--yml" flag. -func parseYML() (*config.YML, error) { - if ymlFlag == nil || *ymlFlag == "" { - return nil, errors.New(flag_yml_usage_unspecified) - } - - // The configuration file is loaded (.yml) - yml, err := config.LoadYML(*ymlFlag) - if err != nil { - return nil, fmt.Errorf("error parsing yml: %w", err) - } - - return yml, nil -} - -// parseArgFilepath parses a filepath to return an absolute filepath to a template directory. -func parseArgFilepath(unknownpath string) (string, error) { - if filepath.Ext(unknownpath) != "" { - return "", fmt.Errorf("specified filepath is not a directory: %q", unknownpath) - } - - // determine the actual filepath of the setup file. - if !filepath.IsAbs(unknownpath) { - if unknownpath, err := filepath.Abs(unknownpath); err != nil { - return "", fmt.Errorf("error while determining the absolute file path of the specified template: %q\n%w", unknownpath, err) - } - } - - return unknownpath, nil -} diff --git a/cmd/constant/external.go b/cmd/constant/external.go new file mode 100644 index 0000000..6ba7dd6 --- /dev/null +++ b/cmd/constant/external.go @@ -0,0 +1,25 @@ +package constant + +// Constants defined by an external entity. +const ( + // OSExitCodeError represents a conventional Linux General Error Exit Code. + OSExitCodeError = 1 + + // FileModeWrite represents the file mode required to overwrite files. + FileModeWrite = 0644 + + // Newline represents a newline character. + Newline byte = '\n' + + // Colon represents a colon character. + Colon byte = ':' + + // Whitesoace represents a whitespace character. + Whitespace byte = ' ' + + // FileExtSQL represents an SQL file extension. + FileExtSQL = ".sql" + + // FileExtGo represents a Go file extension. + FileExtGo = ".go" +) diff --git a/cmd/constant/func.go b/cmd/constant/func.go new file mode 100644 index 0000000..bb59e61 --- /dev/null +++ b/cmd/constant/func.go @@ -0,0 +1,24 @@ +package constant + +import ( + "os" + "path/filepath" +) + +// CopyFile copies a source file to a destination file. +func CopyFile(srcpath, dstpath string) error { + src, err := os.ReadFile(srcpath) + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(dstpath), FileModeWrite); err != nil { + return err + } + + if err := os.WriteFile(dstpath, src, FileModeWrite); err != nil { + return err + } + + return nil +} diff --git a/cmd/constant/internal.go b/cmd/constant/internal.go new file mode 100644 index 0000000..e4adafc --- /dev/null +++ b/cmd/constant/internal.go @@ -0,0 +1,31 @@ +package constant + +import "errors" + +// Constants defined by the program. +const ( + DatabaseConnectionEnvironmentVariableSymbol = '$' + DatabaseSchemaNameDefault = "public" + + DirnameQueriesTemplates = "templates" + DirnameQueriesSchema = "schema" + + DirnameTempQueriesGenerationGo = "dbgoquerygentempgo" + DirnameTempQueriesGenerationSQL = "dbgoquerygentempsql" + DirnameTempQueriesGenerationSQLC = "dbgoquerygentempsqlc" + + FilenameTemplateSchemaGo = "schema.go" + FilenameQueriesSchemaSQL = "schema.sql" + FilenameQueriesCombinedSQL = "combined.sql" + FilenameQueriesCombinedSQLKept = "_dbgo.sql" + + FilenameSQLConfig = "sqlc.yaml" + + PkgNameSchemaGo = "sql" +) + +// Variables defined by the program. +var ( + // ErrYMLDatabaseUnspecified represents an error with the configuration file's database connection value. + ErrYMLDatabaseUnspecified = errors.New("you must specify a database connection ('dbc') in the .yml configuration file") +) diff --git a/cmd/dbgo.go b/cmd/dbgo.go index 47e0307..0008b6a 100644 --- a/cmd/dbgo.go +++ b/cmd/dbgo.go @@ -7,17 +7,18 @@ import ( "os" "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" ) const ( - program_description = `dbgo generates a database consumer package for your database and domain models (i.e., Go types).` + programDescription = `dbgo generates a database consumer package for your database and domain models (i.e., Go types).` ) // cmdDBGO represents the base command when called without any subcommands. var cmdDBGO = &cobra.Command{ Use: "dbgo", - Short: program_description, - Long: program_description, + Short: programDescription, + Long: programDescription, CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, DisableNoDescFlag: false, @@ -31,11 +32,17 @@ var cmdDBGO = &cobra.Command{ // called by main.main() once. func Execute() { if err := cmdDBGO.Execute(); err != nil { - os.Exit(1) + os.Exit(constant.OSExitCodeError) } } func init() { // Persistent Flags work for this command and all subcommands. - cmdDBGO.PersistentFlags().StringVarP(ymlFlag, flag_yml_name, flag_yml_shorthand, "", flag_yml_usage) + cmdDBGO.PersistentFlags().StringVarP( + flagYML, + flagYMLName, + flagYMLShorthand, + "", + flagYMLUsage, + ) } diff --git a/cmd/dbgo_gen.go b/cmd/dbgo_gen.go index b1e6651..a503a87 100644 --- a/cmd/dbgo_gen.go +++ b/cmd/dbgo_gen.go @@ -6,9 +6,14 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" gen "github.com/switchupcb/dbgo/cmd/dbgo_gen" ) +var ( + cmdCombinedFlag = new(bool) +) + // cmdGen represents the dbgo gen command. var cmdGen = &cobra.Command{ Use: "gen", @@ -17,36 +22,40 @@ var cmdGen = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { // check for unexpected arguments if len(args) != 0 { - args_string := strings.Join(args, " ") - fmt.Fprintf(os.Stderr, "Unexpected arguments found: %q", args_string) + argsString := strings.Join(args, " ") + + fmt.Fprintf(os.Stderr, "Unexpected arguments found: %q", argsString) - if args_string == cmdQuery.Use { + if argsString == cmdQuery.Use { fmt.Printf("\n\nDid you mean dbgo %v gen?", cmdQuery.Use) } - os.Exit(1) + os.Exit(constant.OSExitCodeError) } // parse the "--yml" flag. - yml, err := parseYML() + yml, err := parseFlagYML() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } // Run the generator. - output, err := gen.Run(*yml) - if err != nil { + fmt.Println("Generating Go code based on SQL statements.") + + if err := gen.Gen(*yml, *cmdCombinedFlag); err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } - fmt.Println(output) + fmt.Println("\nGenerated Go code based on SQL statements.") }, } func init() { cmdDBGO.AddCommand(cmdGen) + + cmdCombinedFlag = cmdGen.Flags().BoolP("keep", "k", false, "Use --keep to keep a copy of the generated `combined.sql` file in the queries directory.") } diff --git a/cmd/dbgo_gen/gen.go b/cmd/dbgo_gen/gen.go new file mode 100644 index 0000000..431431c --- /dev/null +++ b/cmd/dbgo_gen/gen.go @@ -0,0 +1,195 @@ +package gen + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + + "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" +) + +var ( + newlineBuffer = []byte{constant.Newline} + + // sqlcQueryAnnotationNameIdentifier represents the sqlc Query Annotation name format. + // + // https://docs.sqlc.dev/en/stable/reference/query-annotations.html + sqlcQueryAnnotationNameIdentifier = []byte("-- name: ") + + // sqlcQueryAnnotationCommandExec represents the :exec sqlc query annotation command. + sqlcQueryAnnotationCommandExec = []byte("exec") +) + +// Gen runs dbgo gen programmatically using the given YML. +// +// sqlc is used to generate code from SQL statements. +func Gen(yml config.YML, keepcombined bool) error { + fmt.Println("") + + // Combine the SQL statements in the queries directory to a single SQL file. + printQueryAnnotationGuide := false + + files, err := os.ReadDir(yml.Generated.Input.Queries) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) + + os.Exit(constant.OSExitCodeError) + } + + var combinedFileContentSQL []byte + + for i := range files { + if files[i].IsDir() { + continue + } + + if files[i].Name() == constant.FilenameQueriesCombinedSQLKept { + continue + } + + if path.Ext(files[i].Name()) == constant.FileExtSQL { + src, err := os.ReadFile(filepath.Join(yml.Generated.Input.Queries, files[i].Name())) + if err != nil { + return fmt.Errorf("error reading sql file: %w", err) + } + + // check for the sqlc query annotation line by line. + queryAnnotationExists := false + + srcLines := bytes.Split(src, newlineBuffer) + for i := range srcLines { + if len(srcLines[i]) == 0 { + continue + } + + if bytes.Contains(srcLines[i], sqlcQueryAnnotationNameIdentifier) { + queryAnnotationExists = true + + break + } + } + + // CURRENT: Add `exec` query annotation to the SQL statement when it doesn't exist. + // + // FUTURE: Use a customizable algorithm to automatically add directives when they don't exist. + if !queryAnnotationExists { + printQueryAnnotationGuide = true + + fmt.Printf("WARNING: %v does not have a valid query annotation.\n\tUsing :exec by default.\n", files[i].Name()) + + queryAnnotationName := []byte(files[i].Name()[:len(files[i].Name())-len(constant.FileExtSQL)]) + + // -- name: name :exec + queryAnnotation := make( + []byte, + 0, + len(sqlcQueryAnnotationNameIdentifier)+ + len(queryAnnotationName)+ + 2+ // whitespace + colon + len(sqlcQueryAnnotationCommandExec), + ) + + queryAnnotation = append(queryAnnotation, sqlcQueryAnnotationNameIdentifier...) // `--name: ` + queryAnnotation = append(queryAnnotation, queryAnnotationName...) // name + queryAnnotation = append(queryAnnotation, + constant.Whitespace, // last ' ' in `--name: name ` + constant.Colon, // : + ) + queryAnnotation = append(queryAnnotation, sqlcQueryAnnotationCommandExec...) // exec + + combinedFileContentSQL = append(combinedFileContentSQL, queryAnnotation...) + combinedFileContentSQL = append(combinedFileContentSQL, constant.Newline) + } + + // Add the SQL statement to the combined file. + combinedFileContentSQL = append(combinedFileContentSQL, src...) + combinedFileContentSQL = append(combinedFileContentSQL, constant.Newline, constant.Newline) + } + } + + // Create the sqlc generate project. + // + // dirpathGenerationSpace represents the directory used to contain files during generation. + // + // The dirpathGenerationSpace directory is used when the sqlc CRUD Generator project is created. + dirpathGenerationSpace := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameTempQueriesGenerationSQLC, + ) + + // do not overwrite an existing directory from the user. + if _, err := os.Stat(dirpathGenerationSpace); err == nil { + return fmt.Errorf("directory at must be deleted: %q", dirpathGenerationSpace) + } else if errors.Is(err, os.ErrNotExist) { + + } else { + return fmt.Errorf("error checking directory space: %w", err) + } + + if err := os.MkdirAll(dirpathGenerationSpace, constant.FileModeWrite); err != nil { + return fmt.Errorf("mkdir all: %w", err) + } + + // combined.sql + filepathCombinedSQL := filepath.Join( + dirpathGenerationSpace, + constant.FilenameQueriesCombinedSQL, + ) + + if err := os.WriteFile( + filepathCombinedSQL, + combinedFileContentSQL, + constant.FileModeWrite, + ); err != nil { + return fmt.Errorf("write combined.sql: %w", err) + } + + // sqlc.yaml + srcYML, err := fileContentSQLCYML(yml) + if err != nil { + return fmt.Errorf("write config file: relative pathfinder: %w", err) + } + + filepathSQLCYML := filepath.Join(dirpathGenerationSpace, constant.FilenameSQLConfig) + if err := os.WriteFile( + filepathSQLCYML, + []byte(srcYML), + constant.FileModeWrite, + ); err != nil { + return fmt.Errorf("write sqlc.yaml: %w", err) + } + + // Run sqlc generate. + sqlc := exec.Command("sqlc", "generate", "-f", filepathSQLCYML) + std, err := sqlc.CombinedOutput() + if err != nil { //nolint:wsl + return fmt.Errorf("write Go code: sqlc generate: %q: %w", string(std), err) + } + + // Clean the project. + if keepcombined { + filepathKeptCombinedSQL := filepath.Join( + yml.Generated.Input.Queries, + constant.FilenameQueriesCombinedSQLKept, + ) + + if err := constant.CopyFile(filepathCombinedSQL, filepathKeptCombinedSQL); err != nil { + return fmt.Errorf("error copying combined.sql to queries directory: %w", err) + } + } + + if err := os.RemoveAll(dirpathGenerationSpace); err != nil { + return fmt.Errorf("clean: %w", err) + } + + if printQueryAnnotationGuide { + fmt.Println("\nTIP: You can read more about sqlc query annotations at https://docs.sqlc.dev/en/stable/reference/query-annotations.html") + } + + return nil +} diff --git a/cmd/dbgo_gen/gen_files.go b/cmd/dbgo_gen/gen_files.go new file mode 100644 index 0000000..b1cb75c --- /dev/null +++ b/cmd/dbgo_gen/gen_files.go @@ -0,0 +1,63 @@ +package gen + +import ( + "fmt" + "path/filepath" + + "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" +) + +// fileContentSQLCYML returns the sqlc Generator YML file content. +func fileContentSQLCYML(yml config.YML) (string, error) { + filepathQueriesSQL := filepath.Join( + yml.Generated.Input.Queries, + constant.DirnameTempQueriesGenerationSQLC, + constant.FilenameQueriesCombinedSQL, + ) + + filepathSchemaSQL := filepath.Join( + yml.Generated.Input.Queries, + constant.DirnameQueriesSchema, + constant.FilenameQueriesSchemaSQL, + ) + + pkg := filepath.Base(yml.Generated.Output.DBpkg) + + // find the relative path of each file (relative to sqlc.yaml) + filepathConfig := filepath.Join( + yml.Generated.Input.Queries, + constant.DirnameTempQueriesGenerationSQLC, + ) + + relativeQueriesPath, err := filepath.Rel(filepathConfig, filepathQueriesSQL) + if err != nil { + return "", fmt.Errorf("queries: %w", err) + } + + relativeSchemaPath, err := filepath.Rel(filepathConfig, filepathSchemaSQL) + if err != nil { + return "", fmt.Errorf("queries: %w", err) + } + + relativeOutputPath, err := filepath.Rel(filepathConfig, yml.Generated.Output.DBpkg) + if err != nil { + return "", fmt.Errorf("queries: %w", err) + } + + return fmt.Sprintf(`version: "2" +sql: + - engine: "postgresql" + queries: "%s" + schema: "%s" + gen: + go: + package: "%s" + out: "%s" + sql_package: "pgx/v5"`, + filepath.ToSlash(relativeQueriesPath), // queries + filepath.ToSlash(relativeSchemaPath), // schema + pkg, // package + filepath.ToSlash(relativeOutputPath), // out + ), nil +} diff --git a/cmd/dbgo_gen/run.go b/cmd/dbgo_gen/run.go deleted file mode 100644 index 6f2c01c..0000000 --- a/cmd/dbgo_gen/run.go +++ /dev/null @@ -1,10 +0,0 @@ -package gen - -import ( - "github.com/switchupcb/dbgo/cmd/config" -) - -// Run runs dbgo gen programmatically using the given YML. -func Run(yml config.YML) (string, error) { - return "This feature is implemented on March 10, 2025.", nil -} diff --git a/cmd/dbgo_query/constant.go b/cmd/dbgo_query/constant.go deleted file mode 100644 index 213afa1..0000000 --- a/cmd/dbgo_query/constant.go +++ /dev/null @@ -1,16 +0,0 @@ -package query - -const ( - writeFileMode = 0644 - fileExtSQL = ".sql" - fileExtGo = ".go" - - sqlGoDir = "go" - queriesGoTemplatesDirname = "templates" - - newline = '\n' - colon = ':' - whitespace = ' ' - - err_database_unspecified = "you must specify a database connection ('dbc') in the .yml configuration file" -) diff --git a/cmd/dbgo_query/gen.go b/cmd/dbgo_query/gen.go index f308a6b..d28b76d 100644 --- a/cmd/dbgo_query/gen.go +++ b/cmd/dbgo_query/gen.go @@ -9,79 +9,105 @@ import ( "path/filepath" "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" ) -const ( - generatedQueriesDirname = "dbgoquerygentemp" - generatedQueriesOutputDirname = "output" - generatedQueriesSchemaSQLFilename = "schema.sql" -) - -// Gen runs dbgo query programmatically using the given YML. -func Gen(yml config.YML) (string, error) { - if yml.Generated.Input.DB.Connection == "" { - return "", errors.New(err_database_unspecified) - } +// Gen runs dbgo query gen programmatically using the given YML. +func Gen(yml config.YML) error { + filepathSchemaSQL := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesSchema, // schema + constant.FilenameQueriesSchemaSQL, // schema.sql + ) - if yml.Generated.Input.DB.Connection[0] == databaseConnectionEnvironmentVariableSymbol { - yml.Generated.Input.DB.Connection = os.Getenv(yml.Generated.Input.DB.Connection[1:]) + if _, err := os.Stat(filepathSchemaSQL); err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf( //nolint:stylecheck // ST1005 + `schema.sql was not found at %q + You can use 'db query schema' to regenerate a schema.sql file. + Then, run 'db query gen' again to generate CRUD SQL.`, + filepathSchemaSQL, + ) + } else { + return fmt.Errorf("error checking .../queries/schema/schema.go file space: %w", err) + } } - generatedQueriesFilepath := filepath.Join( + // dirpathGenerationSpace represents the directory used to contain files during generation. + // + // The dirpathGenerationSpace directory is used when the sqlc CRUD Generator project is created. + dirpathGenerationSpace := filepath.Join( yml.Generated.Input.Queries, // queries - generatedQueriesDirname, // generatedQueriesDirname + constant.DirnameTempQueriesGenerationSQL, ) - if _, err := os.Stat(generatedQueriesFilepath); err == nil { - return "", fmt.Errorf("warning: directory at must be deleted: %q", generatedQueriesFilepath) + // Do not overwrite an existing directory from the user. + if _, err := os.Stat(dirpathGenerationSpace); err == nil { + return fmt.Errorf("directory at must be deleted: %q", dirpathGenerationSpace) } else if errors.Is(err, os.ErrNotExist) { } else { - return "", fmt.Errorf("error checking for directory space: %w", err) + return fmt.Errorf("error checking directory space: %w", err) } - // Create an sqlc CRUD Generator project. - if err := os.MkdirAll(generatedQueriesFilepath, writeFileMode); err != nil { - return "", fmt.Errorf("mkdir all: %w", err) + // Create an sqlc CRUD Generator project directory. + if err := os.MkdirAll(dirpathGenerationSpace, constant.FileModeWrite); err != nil { + return fmt.Errorf("mkdir all: %w", err) } - // Add schema file. - pgdump := exec.Command("pg_dump", //nolint:gosec // disable G204 - yml.Generated.Input.DB.Connection, - "--schema-only", - "-f", filepath.Join(generatedQueriesFilepath, generatedQueriesSchemaSQLFilename), - ) + // Add static files. + // + // placeholder.sql + if err := os.WriteFile( + filepath.Join( + dirpathGenerationSpace, + filenamePlaceholderSQL, + ), + []byte(fileContentPlaceholderSQL), + constant.FileModeWrite, + ); err != nil { + return fmt.Errorf("write placeholder file: %w", err) + } - std, err := pgdump.CombinedOutput() + // crud.json + relativeSchemaPath, err := filepath.Rel(dirpathGenerationSpace, filepathSchemaSQL) if err != nil { - return "", fmt.Errorf("write schema file: pg_dump: %q: %w", string(std), err) + return fmt.Errorf("write config file: relative pathfinder: %w", err) } - // Add static files. - if err := os.WriteFile(filepath.Join(generatedQueriesFilepath, file_name_dummy_sql), []byte(file_content_dummy_sql), writeFileMode); err != nil { - return "", fmt.Errorf("write dummy file: %w", err) - } + filepathSQLCJSON := filepath.Join( + dirpathGenerationSpace, + filenameSQLCJSON, + ) - file_path_sqlc_json := filepath.Join(generatedQueriesFilepath, file_name_sqlc_json) - if err := os.WriteFile(file_path_sqlc_json, []byte(file_content_sqlc_json), writeFileMode); err != nil { - return "", fmt.Errorf("write config file: %w", err) + if err := os.WriteFile( + filepathSQLCJSON, + []byte(fileContentSQLCJSON(relativeSchemaPath)), + constant.FileModeWrite, + ); err != nil { + return fmt.Errorf("write config file: %w", err) } // Run the CRUD Generator. - sqlc := exec.Command("sqlc", "generate", "-f", file_path_sqlc_json) - - std, err = sqlc.CombinedOutput() - if err != nil { - return "", fmt.Errorf("write CRUD SQL: sqlc generate: %q: %w", string(std), err) + sqlc := exec.Command("sqlc", "generate", "-f", filepathSQLCJSON) + std, err := sqlc.CombinedOutput() + if err != nil { //nolint:wsl + return fmt.Errorf("write CRUD SQL: sqlc generate: %q: %w", string(std), err) } // Output the CRUD SQL to the queries directory. - src, err := os.ReadFile(filepath.Join(generatedQueriesFilepath, generatedQueriesOutputDirname, file_name_dummy_sql)) + src, err := os.ReadFile( + filepath.Join( + dirpathGenerationSpace, + dirnameGeneratedQueriesOutput, + filenameGeneratedQueriesSQL, + ), + ) if err != nil { - return "", fmt.Errorf("read CRUD SQL: %w", err) + return fmt.Errorf("read CRUD SQL: %w", err) } - srcQueries := bytes.Split(src, []byte{newline, newline}) + srcQueries := bytes.Split(src, []byte{constant.Newline, constant.Newline}) for i := range srcQueries { query := srcQueries[i] @@ -92,20 +118,20 @@ func Gen(yml config.YML) (string, error) { // name represents the query name (e.g., `InsertUser` in `-- name: InsertUser :one`) var name []byte - colon_count := 0 + colonCount := 0 parseName: for i := range query { - switch colon_count { + switch colonCount { case 0: - if query[i] == colon { - colon_count++ + if query[i] == constant.Colon { + colonCount++ } case 1: switch query[i] { - case whitespace: - case colon: - colon_count++ + case constant.Whitespace: + case constant.Colon: + colonCount++ default: name = append(name, query[i]) } @@ -114,18 +140,18 @@ func Gen(yml config.YML) (string, error) { } } - if colon_count != 2 { //nolint:mnd - return "", fmt.Errorf("encountered invalid CRUD SQL at statement %d\n%q", i, string(srcQueries[i])) + if colonCount != 2 { //nolint:mnd + return fmt.Errorf("encountered invalid CRUD SQL at statement %d\n%q", i, string(srcQueries[i])) } - if err := os.WriteFile(filepath.Join(yml.Generated.Input.Queries, string(name)+fileExtSQL), query, writeFileMode); err != nil { - return "", fmt.Errorf("write CRUD SQL FILE at statement %d\n%q", i, string(query)) + if err := os.WriteFile(filepath.Join(yml.Generated.Input.Queries, string(name)+constant.FileExtSQL), query, constant.FileModeWrite); err != nil { + return fmt.Errorf("write CRUD SQL FILE at statement %d\n%q", i, string(query)) } } - if err := os.RemoveAll(generatedQueriesFilepath); err != nil { - return "", fmt.Errorf("clean: %w", err) + if err := os.RemoveAll(dirpathGenerationSpace); err != nil { + return fmt.Errorf("clean: %w", err) } - return fmt.Sprintf("Generated CRUD SQL files at %q", yml.Generated.Input.Queries), nil + return nil } diff --git a/cmd/dbgo_query/gen_files.go b/cmd/dbgo_query/gen_files.go index 0232046..a0da847 100644 --- a/cmd/dbgo_query/gen_files.go +++ b/cmd/dbgo_query/gen_files.go @@ -1,13 +1,25 @@ package query +import ( + "fmt" + "path/filepath" +) + const ( - file_name_dummy_sql = "query.sql" - file_content_dummy_sql = `-- name: dummy :one + dirnameGeneratedQueriesOutput = "output" + filenameGeneratedQueriesSQL = "query.sql" + + filenamePlaceholderSQL = "placeholder.sql" + fileContentPlaceholderSQL = `-- name: placeholder :one SELECT current_timestamp; ` - file_name_sqlc_json = "crud.json" - file_content_sqlc_json = `{ + filenameSQLCJSON = "crud.json" +) + +// fileContentSQLCJSON returns the sqlc CRUD Generator JSON file content. +func fileContentSQLCJSON(schemapath string) string { + return `{ "version": "2", "plugins": [ { @@ -18,19 +30,22 @@ SELECT current_timestamp; } } ], - "sql": [ + "sql": [` + + fmt.Sprintf(` { - "schema": "schema.sql", - "queries": "query.sql", + "schema": "%s", + "queries": "%s", "engine": "postgresql", "codegen": [ { - "out": "output", + "out": "%s", "plugin": "gen-crud", "options": {} } ] } + `, filepath.ToSlash(schemapath), filenamePlaceholderSQL, dirnameGeneratedQueriesOutput) + + ` ] }` -) +} diff --git a/cmd/dbgo_query/save.go b/cmd/dbgo_query/save.go index ab0cf0f..93e262b 100644 --- a/cmd/dbgo_query/save.go +++ b/cmd/dbgo_query/save.go @@ -1,35 +1,51 @@ package query import ( + "errors" "fmt" "os" "path/filepath" "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" ) -// Save runs dbgo query save programmatically using the given filepath and YML. -func Save(abspath string, yml config.YML) (string, error) { - filename := filepath.Base(abspath) - sql_filename := filename + fileExtSQL - sql_filepath := filepath.Join(yml.Generated.Input.Queries, sql_filename) - - fmt.Printf("SAVING QUERY %v from template at %v\n", sql_filename, abspath) +// Save runs dbgo query save programmatically using the given template name and YML. +func Save(name string, yml config.YML) error { + templateFilepath := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesTemplates, // templates + name, // template (name) + ) + + if _, err := os.Stat(templateFilepath); err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf( //nolint:stylecheck // ST1005 + `template was not found at %q + You can use 'db query template %v' to create a template. + Then, run 'db query save %v' again to save the SQL output to an SQL file.`, + templateFilepath, name, name, + ) + } else { + return fmt.Errorf("error checking template file space: %w", err) + } + } // sqlcode is returned from an interpreted function which returns type-safe SQL in a string. - sqlcode, err := interpretFunction(abspath) + sqlcode, err := interpretFunction(templateFilepath) if err != nil { - return "", fmt.Errorf("interpreter: %w", err) + return fmt.Errorf("interpreter: %w", err) } - if sqlcode[0] == newline { + if len(sqlcode) > 1 && sqlcode[0] == constant.Newline { sqlcode = sqlcode[1:] } // write output to an sql file with the same name as the interpreted file. - if err := os.WriteFile(sql_filepath, []byte(sqlcode), writeFileMode); err != nil { - return "", fmt.Errorf("error creating sql file: %w", err) + filepathSQL := filepath.Join(yml.Generated.Input.Queries, name+constant.FileExtSQL) + if err := os.WriteFile(filepathSQL, []byte(sqlcode), constant.FileModeWrite); err != nil { + return fmt.Errorf("error creating sql file: %w", err) } - return fmt.Sprintf("%v QUERY SAVED from template at %v", sql_filename, abspath), nil + return nil } diff --git a/cmd/dbgo_query/save_interpreter.go b/cmd/dbgo_query/save_interpreter.go index 076c1c8..e052ba3 100644 --- a/cmd/dbgo_query/save_interpreter.go +++ b/cmd/dbgo_query/save_interpreter.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/switchupcb/dbgo/cmd/constant" "github.com/switchupcb/dbgo/cmd/dbgo_query/extract" "github.com/traefik/yaegi/interp" "github.com/traefik/yaegi/stdlib" @@ -17,7 +18,7 @@ const interpretedFunctionName = "SQL" func interpretFunction(dirpath string) (string, error) { _, err := os.ReadDir(dirpath) if err != nil { - return "", fmt.Errorf("error loading template: %v\nIs the relative or absolute filepath set correctly?\n%w", dirpath, err) + return "", fmt.Errorf("error loading template: %v\n\tIs the relative or absolute filepath set correctly?\n%w", dirpath, err) } // setup the interpreter @@ -37,16 +38,16 @@ func interpretFunction(dirpath string) (string, error) { } // load the source (in a specific order) - if _, err := i.EvalPath(filepath.Join(dirpath, templateGoSchemaFilename)); err != nil { + if _, err := i.EvalPath(filepath.Join(dirpath, constant.FilenameTemplateSchemaGo)); err != nil { return "", fmt.Errorf("error compiling template schema.go file: %w", err) } - if _, err := i.EvalPath(filepath.Join(dirpath, filepath.Base(dirpath)+fileExtGo)); err != nil { + if _, err := i.EvalPath(filepath.Join(dirpath, filepath.Base(dirpath)+constant.FileExtGo)); err != nil { return "", fmt.Errorf("error compiling template.go file: %w", err) } // load the func from the interpreter - interpretedFunction := filepath.Base(dirpath) + "." + interpretedFunctionName + interpretedFunction := constant.PkgNameSchemaGo + "." + interpretedFunctionName v, err := i.Eval(interpretedFunction) if err != nil { @@ -60,12 +61,12 @@ func interpretFunction(dirpath string) (string, error) { defer func() { if r := recover(); r != nil { - r_msg, ok := r.(string) + recoverMsg, ok := r.(string) if !ok { fmt.Println("impossible recovery") } - fmt.Printf("\t%v", r_msg) + fmt.Printf("\t%v", recoverMsg) os.Exit(1) } @@ -73,7 +74,7 @@ func interpretFunction(dirpath string) (string, error) { content, err := fn() if err != nil { - return "", fmt.Errorf("%w", err) + return "", fmt.Errorf("SQL(): %w", err) } return content, nil diff --git a/cmd/dbgo_query/schema.go b/cmd/dbgo_query/schema.go new file mode 100644 index 0000000..2f46199 --- /dev/null +++ b/cmd/dbgo_query/schema.go @@ -0,0 +1,203 @@ +package query + +import ( + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + + "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" + "github.com/switchupcb/jet/v2/generator/postgres" +) + +// Schema runs dbgo query schema programmatically using the given YML. +func Schema(yml config.YML, schemago, schemasql bool) error { + var err error + + yml.Generated.Input.DB.Connection, err = validatedDatabaseConnection(yml) + if err != nil { + return err + } + + if yml.Generated.Input.DB.Schema == "" { + yml.Generated.Input.DB.Schema = constant.DatabaseSchemaNameDefault + } + + queriesSchemaDir := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesSchema, // schema + ) + + if schemago { + fmt.Printf("\nGenerating schema.go in %q\n", queriesSchemaDir) + + if err := SchemaGo( + queriesSchemaDir, + yml.Generated.Input.DB.Connection, + yml.Generated.Input.DB.Schema, + ); err != nil { + return fmt.Errorf("error generating schema.go: %w", err) + } + + fmt.Printf("Generated schema.go in %q\n", queriesSchemaDir) + } + + if schemasql { + fmt.Printf("\nGenerating schema.sql in %q\n", queriesSchemaDir) + + if err := SchemaSQL( + queriesSchemaDir, + yml.Generated.Input.DB.Connection, + yml.Generated.Input.DB.Schema, + ); err != nil { + return fmt.Errorf("error generating schema.go: %w", err) + } + + fmt.Printf("Generated schema.sql in %q\n", queriesSchemaDir) + } + + return nil +} + +// SchemaSQL generates a schema.sql file in the given directory using the +// database connection string and database schema name. +func SchemaSQL(dirpath, dbconnection, dbschema string) error { + pgdump := exec.Command("pg_dump", //nolint:gosec // disable G204 + dbconnection, + "-n", dbschema, + "--schema-only", + "-f", filepath.Join(dirpath, constant.FilenameQueriesSchemaSQL), + ) + + std, err := pgdump.CombinedOutput() + if err != nil { + return fmt.Errorf("write schema file: pg_dump: %q: %w", string(std), err) + } + + return nil +} + +// SchemaGo generates a schema.go file in the given directory using the +// database connection string and database schema name. +func SchemaGo(dirpath, dbconnection, dbschema string) error { + // dirpathGenerationSpace represents the directory used to contain files during generation. + // + // The dirpathGenerationSpace directory is used when Jet generates files from the database. + dirpathGenerationSpace := filepath.Join( + dirpath, // (e.g., `.../queries/schema` from `.../queries/schema/schema.go`) + constant.DirnameTempQueriesGenerationGo, + ) + + // Do not overwrite an existing directory from the user. + if _, err := os.Stat(dirpathGenerationSpace); err == nil { + return fmt.Errorf("warning: directory at must be deleted: %q", dirpathGenerationSpace) + } else if errors.Is(err, os.ErrNotExist) { + + } else { + return fmt.Errorf("error checking for directory space: %w", err) + } + + // Generate the database schema models as Go types. + generatorTemplate := genTemplate() + if err := postgres.GenerateDSN( + dbconnection, + dbschema, + dirpathGenerationSpace, + generatorTemplate, + ); err != nil { + return fmt.Errorf("jet: %w", err) + } + + fmt.Println("Generated schema as models.") + + // Merge generated files to a single schema.go file. + fileContentSchemas := [][]byte{ + []byte("package " + constant.PkgNameSchemaGo + "\n\nimport \"github.com/switchupcb/jet/v2/postgres\""), + } + + if err := filepath.WalkDir(dirpathGenerationSpace, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + // Do not merge generated files from the model directory. + if filepath.Base(path) == "model" { + return nil + } + + // Do not attempt to merge directories with 0 files. + fileCount, err := countDirFiles(path) + if err != nil { + return fmt.Errorf("directory file count: %w", err) + } + + if fileCount == 0 { + return nil + } + + xstructOutput, err := xstruct(path, constant.PkgNameSchemaGo) + if err != nil { + return fmt.Errorf("xstruct called from %q: %w", path, err) + } + + fileContentSchemas = append(fileContentSchemas, xstructOutput) + } + + return nil + }); err != nil { + return fmt.Errorf("error flattening structs from generated SQL Go types: %w", err) + } + + merger := newMerger(constant.PkgNameSchemaGo) + for i := range fileContentSchemas { + if err := merger.parseFile("", fileContentSchemas[i]); err != nil { + return fmt.Errorf("merge: file_content_schema: %w\n\n%v", err, string(fileContentSchemas[i])) + } + } + + delete(merger.addedImports, "\"github.com/go-jet/jet/postgres\"") + + if err := merger.WriteToFile(filepath.Join(dirpath, constant.FilenameTemplateSchemaGo)); err != nil { + return fmt.Errorf("merge: write: %w", err) + } + + if err := os.RemoveAll(dirpathGenerationSpace); err != nil { + return fmt.Errorf("merge: clean: %w", err) + } + + return nil +} + +// countDirFiles counts the number of non-directory files in a directory. +func countDirFiles(dirpath string) (int, error) { + file, err := os.Open(dirpath) + if err != nil { + return 0, err //nolint:wrapcheck + } + + defer file.Close() + + list, err := file.Readdirnames(-1) + if err != nil { + return 0, err //nolint:wrapcheck + } + + var fileCount int + + for i := range list { + fileInfo, err := os.Stat(filepath.Join(dirpath, list[i])) + if err != nil { + return 0, err //nolint:wrapcheck + } + + if !fileInfo.IsDir() { + fileCount++ + } + } + + return fileCount, nil +} diff --git a/cmd/dbgo_query/template_jet.go b/cmd/dbgo_query/schema_jet.go similarity index 98% rename from cmd/dbgo_query/template_jet.go rename to cmd/dbgo_query/schema_jet.go index 285fc72..68839a9 100644 --- a/cmd/dbgo_query/template_jet.go +++ b/cmd/dbgo_query/schema_jet.go @@ -1,6 +1,7 @@ package query import ( + _ "github.com/lib/pq" //nolint:revive "github.com/switchupcb/jet/v2/generator/metadata" "github.com/switchupcb/jet/v2/generator/template" "github.com/switchupcb/jet/v2/postgres" diff --git a/cmd/dbgo_query/template_merger.go b/cmd/dbgo_query/schema_merger.go similarity index 93% rename from cmd/dbgo_query/template_merger.go rename to cmd/dbgo_query/schema_merger.go index d8af041..e0184b9 100644 --- a/cmd/dbgo_query/template_merger.go +++ b/cmd/dbgo_query/schema_merger.go @@ -31,7 +31,7 @@ import ( "sort" ) -type Merger struct { +type merger struct { tree *ast.File addedImports map[string]ast.Spec addedConsts map[string]ast.Spec @@ -41,8 +41,8 @@ type Merger struct { specialFunc map[string]bool } -func NewMerger(pkg string) *Merger { - merger := &Merger{ +func newMerger(pkg string) *merger { + merger := &merger{ tree: &ast.File{ Name: ast.NewIdent(pkg), }, @@ -57,7 +57,7 @@ func NewMerger(pkg string) *Merger { return merger } -func (m *Merger) parseFile(path string, src any) error { +func (m *merger) parseFile(path string, src any) error { fset := token.NewFileSet() file, err := parser.ParseFile(fset, path, src, 0) @@ -90,7 +90,7 @@ func (m *Merger) parseFile(path string, src any) error { return nil } -func (m *Merger) parseGenDecl(decl *ast.GenDecl) { +func (m *merger) parseGenDecl(decl *ast.GenDecl) { switch decl.Tok { case token.IMPORT: for _, spec := range decl.Specs { @@ -123,7 +123,7 @@ func (m *Merger) parseGenDecl(decl *ast.GenDecl) { } } -func (m *Merger) buildGenDecl() { +func (m *merger) buildGenDecl() { var specs []ast.Spec specs = make([]ast.Spec, 0, len(m.addedImports)) @@ -171,7 +171,7 @@ func (m *Merger) buildGenDecl() { } } -func (m *Merger) sortAddedFuncs() []*ast.FuncDecl { +func (m *merger) sortAddedFuncs() []*ast.FuncDecl { keys := make([]string, 0, len(m.addedFunc)) for k := range m.addedFunc { if _, ok := m.specialFunc[k]; ok { @@ -197,7 +197,7 @@ func (m *Merger) sortAddedFuncs() []*ast.FuncDecl { return sortedFuncs } -func (m *Merger) WriteToFile(sourceName string) error { +func (m *merger) WriteToFile(sourceName string) error { source, err := os.Create(sourceName) if err != nil { return err diff --git a/cmd/dbgo_query/schema_xstruct.go b/cmd/dbgo_query/schema_xstruct.go new file mode 100644 index 0000000..b95e51c --- /dev/null +++ b/cmd/dbgo_query/schema_xstruct.go @@ -0,0 +1,30 @@ +package query + +import ( + xstructConfig "github.com/switchupcb/xstruct/cli/config" + xstructGen "github.com/switchupcb/xstruct/cli/generator" + xstructParser "github.com/switchupcb/xstruct/cli/parser" + "golang.org/x/tools/imports" +) + +// xstruct runs xstruct programmatically using the given path and package name. +func xstruct(dirpath, pkg string) ([]byte, error) { + gen, err := xstructConfig.LoadFiles(dirpath) + if err != nil { + return nil, err + } + + if err = xstructParser.Parse(gen, true, true); err != nil { + return nil, err + } + + content := xstructGen.AstWriteDecls(pkg, gen.ASTDecls, gen.FuncDecls) + + // imports + importsdata, err := imports.Process("", content, nil) + if err != nil { + return nil, err + } + + return importsdata, nil +} diff --git a/cmd/dbgo_query/template.go b/cmd/dbgo_query/template.go index 5e96cda..7858e23 100644 --- a/cmd/dbgo_query/template.go +++ b/cmd/dbgo_query/template.go @@ -3,22 +3,16 @@ package query import ( "errors" "fmt" - "io/fs" "os" - "os/exec" "path/filepath" - _ "github.com/lib/pq" "github.com/switchupcb/dbgo/cmd/config" - "github.com/switchupcb/jet/v2/generator/postgres" + "github.com/switchupcb/dbgo/cmd/constant" ) const ( - databaseConnectionEnvironmentVariableSymbol = '$' - - templateGoSchemaFilename = "schema.go" - - file_content_static = ` + interpretedFileContentStatic = "package " + constant.PkgNameSchemaGo + + ` import . "github.com/switchupcb/jet/v2/postgres" @@ -37,156 +31,64 @@ func SQL() (string, error) { ` ) -// Template runs dbgo query template programmatically using the given filepath and YML. -func Template(abspath string, yml config.YML) (string, error) { - if yml.Generated.Input.DB.Connection == "" { - return "", errors.New(err_database_unspecified) - } - - if yml.Generated.Input.DB.Connection[0] == databaseConnectionEnvironmentVariableSymbol { - yml.Generated.Input.DB.Connection = os.Getenv(yml.Generated.Input.DB.Connection[1:]) - } - - if yml.Generated.Input.DB.Schema == "" { - yml.Generated.Input.DB.Schema = "public" - } - - template_name := filepath.Base(abspath) - - fmt.Printf("ADDING TEMPLATE %v to %v\n\n", template_name, abspath) +// Template runs dbgo query template programmatically using the given template name and YML. +func Template(name string, yml config.YML) error { + // Copy the existing schema.go file for Go type autocompletion within the template. + copied := true - // Generate the database schema models as Go types. - sqlGoDirpath := filepath.Join( - yml.Generated.Input.Queries, // queries - queriesGoTemplatesDirname, // templates - template_name, // template (name) - sqlGoDir, // go + queriesSchemaGoFilepath := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesSchema, // schema + constant.FilenameTemplateSchemaGo, // schema.go ) - generatorTemplate := genTemplate() - if err := postgres.GenerateDSN( - yml.Generated.Input.DB.Connection, - yml.Generated.Input.DB.Schema, - sqlGoDirpath, - generatorTemplate, - ); err != nil { - return "", fmt.Errorf("%w", err) - } - - fmt.Println("Generated schema as models.") - fmt.Println() - - // Merge generated files to a single schema.go file. - file_content_schemas := [][]byte{ - []byte("package " + template_name + "\n\n" + "import \"github.com/switchupcb/jet/v2/postgres\""), - } - - if err := filepath.WalkDir(sqlGoDirpath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - if filepath.Base(path) == "model" { - return nil - } - - file_count, err := countDirFiles(path) - if err != nil { - return fmt.Errorf("directory file count: %w", err) - } - - if file_count == 0 { - return nil - } - - xstruct := exec.Command("xstruct", "-d", path, "-p", template_name, "-f", "-g") - std, err := xstruct.CombinedOutput() - if err != nil { - return fmt.Errorf("xstruct called from %q: %v", path, string(std)) - } + templateSchemaGoFilepath := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesTemplates, // templates + name, // template (name) + constant.FilenameTemplateSchemaGo, // schema.go + ) - file_content_schemas = append(file_content_schemas, std) + if _, err := os.Stat(queriesSchemaGoFilepath); err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Println( + "WARNING: The template's schema.go file was not updated because" + + "schema.go was not found at " + queriesSchemaGoFilepath + + "\n\tYou can use `db query schema` to regenerate a schema.go file." + + "\n\tThen, run `db query template` again to update the template's schema.go file.", + ) + + copied = false + } else { + return fmt.Errorf("error checking .../queries/schema/schema.go file space: %w", err) } - - return nil - }); err != nil { - return "", fmt.Errorf("error flattening structs from generated SQL Go types: %w", err) } - merger := NewMerger(template_name) - for i := range file_content_schemas { - if err := merger.parseFile("", file_content_schemas[i]); err != nil { - return "", fmt.Errorf("merge: file_content_schema: %w\n\n%v", err, string(file_content_schemas[i])) + if copied { + if err := constant.CopyFile(queriesSchemaGoFilepath, templateSchemaGoFilepath); err != nil { + return fmt.Errorf("error copying queries schema.go to template: %w", err) } } - delete(merger.addedImports, "\"github.com/go-jet/jet/postgres\"") - - templateSchemaFilepath := filepath.Join( - yml.Generated.Input.Queries, // queries - queriesGoTemplatesDirname, // templates - template_name, // template (name) - templateGoSchemaFilename, // schema.go - ) - - if err := merger.WriteToFile(templateSchemaFilepath); err != nil { - return "", fmt.Errorf("merge: write: %w", err) - } - - if err := os.RemoveAll(sqlGoDirpath); err != nil { - return "", fmt.Errorf("merge: clean: %w", err) - } - - // Create the interpreted function file. - file_content := []byte("package " + template_name + file_content_static) - - templateFilepath := filepath.Join( - yml.Generated.Input.Queries, // queries - queriesGoTemplatesDirname, // templates - template_name, // template (name) - template_name+fileExtGo, // template.go + // Create the interpreted function file when it doesn't exist. + templateInterpretedGoFilepath := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesTemplates, // templates + name, // template (name) + name+constant.FileExtGo, // template.go ) - if _, err := os.Stat(templateFilepath); err == nil { - - } else if errors.Is(err, os.ErrNotExist) { - if err := os.WriteFile(templateFilepath, file_content, writeFileMode); err != nil { - return "", fmt.Errorf("template: write: %w", err) - } - } else { - return "", fmt.Errorf("error checking for template file space: %w", err) - } + if _, err := os.Stat(templateInterpretedGoFilepath); err != nil { + if errors.Is(err, os.ErrNotExist) { + fileContent := []byte(interpretedFileContentStatic) - return fmt.Sprintf("ADDED TEMPLATE %q to %v", template_name, filepath.Dir(templateFilepath)), nil -} - -// countDirFiles counts the number of non-directory files in a directory. -func countDirFiles(dirpath string) (int, error) { - file, err := os.Open(dirpath) - if err != nil { - return 0, err - } - - defer file.Close() - - list, err := file.Readdirnames(-1) - if err != nil { - return 0, err - } - - var file_count int - - for i := range list { - file_info, err := os.Stat(filepath.Join(dirpath, list[i])) - if err != nil { - return 0, err - } - - if !file_info.IsDir() { - file_count++ + if err := os.WriteFile(templateInterpretedGoFilepath, fileContent, constant.FileModeWrite); err != nil { + return fmt.Errorf("template: write: %w", err) + } + } else { + return fmt.Errorf("error checking template file space: %w", err) } } - return file_count, nil + return nil } diff --git a/cmd/dbgo_query/validate.go b/cmd/dbgo_query/validate.go new file mode 100644 index 0000000..7fbc546 --- /dev/null +++ b/cmd/dbgo_query/validate.go @@ -0,0 +1,25 @@ +package query + +import ( + "os" + + "github.com/switchupcb/dbgo/cmd/config" + "github.com/switchupcb/dbgo/cmd/constant" +) + +// validatedDatabaseConnection returns a validated Database Connection string from the given YML. +func validatedDatabaseConnection(yml config.YML) (string, error) { + if yml.Generated.Input.DB.Connection == "" { + return "", constant.ErrYMLDatabaseUnspecified + } + + if yml.Generated.Input.DB.Connection[0] == constant.DatabaseConnectionEnvironmentVariableSymbol { + yml.Generated.Input.DB.Connection = os.Getenv(yml.Generated.Input.DB.Connection[1:]) + + if yml.Generated.Input.DB.Connection == "" { + return "", constant.ErrYMLDatabaseUnspecified + } + } + + return yml.Generated.Input.DB.Connection, nil +} diff --git a/cmd/dbgo_query_gen.go b/cmd/dbgo_query_gen.go index ebd2270..5e9f8c1 100644 --- a/cmd/dbgo_query_gen.go +++ b/cmd/dbgo_query_gen.go @@ -6,44 +6,45 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" query "github.com/switchupcb/dbgo/cmd/dbgo_query" ) const ( - subcommand_description_gen = "Generates SQL queries for Read (Select) operations and Create (Insert), Update, Delete operations." + subcommandDescriptionGen = "Generates SQL queries for Read (Select) operations and Create (Insert), Update, Delete operations." ) // cmdQueryGen represents the dbgo query gen command. var cmdQueryGen = &cobra.Command{ Use: "gen", Short: "Generates SQL statements from your database.", - Long: subcommand_description_gen, + Long: subcommandDescriptionGen, Run: func(cmd *cobra.Command, args []string) { // check for unexpected arguments if len(args) != 0 { - args_string := strings.Join(args, " ") - fmt.Fprintf(os.Stderr, "Unexpected arguments found: %q", args_string) + argsString := strings.Join(args, " ") - os.Exit(1) + fmt.Fprintf(os.Stderr, "Unexpected arguments found: %q", argsString) + + os.Exit(constant.OSExitCodeError) } // parse the "--yml" flag. - yml, err := parseYML() + yml, err := parseFlagYML() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } // Run the generator. - output, err := query.Gen(*yml) - if err != nil { + if err := query.Gen(*yml); err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } - fmt.Println(output) + fmt.Println("Generated CRUD SQL files at \"" + yml.Generated.Input.Queries + "\"") }, } diff --git a/cmd/dbgo_query_save.go b/cmd/dbgo_query_save.go index 181fe29..2da0878 100644 --- a/cmd/dbgo_query_save.go +++ b/cmd/dbgo_query_save.go @@ -6,49 +6,41 @@ import ( "path/filepath" "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" query "github.com/switchupcb/dbgo/cmd/dbgo_query" ) const ( - subcommand_description_save = "Saves an SQL file (with the same name as the template [e.g., `name.sql`]) containing an SQL statement (returned from the `SQL()` function in `name.go`) to the queries directory." + subcommandDescriptionSave = "Saves an SQL file (with the same name as the template [e.g., `name.sql`]) containing an SQL statement (returned from the `SQL()` function in `name.go`) to the queries directory." ) // cmdSave represents the dbgo query save command. var cmdSave = &cobra.Command{ Use: "save", Short: "Save a type-safe SQL file to your queries directory.", - Long: subcommand_description_save, + Long: subcommandDescriptionSave, Run: func(cmd *cobra.Command, args []string) { // parse the "--yml" flag. - yml, err := parseYML() + yml, err := parseFlagYML() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } - // run the generator for each template. + // add each template to the filepath arguments when no filepath arguments are provided. if len(args) == 0 { - queriesGoTemplatesDir := filepath.Join(yml.Generated.Input.Queries, queriesGoTemplatesDirname) + queriesGoTemplatesDir := filepath.Join(yml.Generated.Input.Queries, constant.DirnameQueriesTemplates) files, err := os.ReadDir(queriesGoTemplatesDir) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } for i := range files { - output, err := query.Save(filepath.Join(queriesGoTemplatesDir, files[i].Name()), *yml) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n\n", fmt.Errorf("%w", err)) - - continue - } - - fmt.Printf("%v\n\n", output) + args = append(args, filepath.Join(queriesGoTemplatesDir, files[i].Name())) } - - return } // run the generator for each filepath argument. @@ -60,14 +52,19 @@ var cmdSave = &cobra.Command{ continue } - output, err := query.Save(abspath, *yml) - if err != nil { + templateName := filepath.Base(abspath) + + sqlFilename := templateName + constant.FileExtSQL + + fmt.Printf("SAVING QUERY %v from template at %v\n", sqlFilename, abspath) + + if err := query.Save(templateName, *yml); err != nil { fmt.Fprintf(os.Stderr, "%v\n\n", fmt.Errorf("%w", err)) continue } - fmt.Printf("%v\n\n", output) + fmt.Printf("%v QUERY SAVED from template at %v\n\n", sqlFilename, abspath) } }, } diff --git a/cmd/dbgo_query_schema.go b/cmd/dbgo_query_schema.go new file mode 100644 index 0000000..9dd6ac1 --- /dev/null +++ b/cmd/dbgo_query_schema.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" + query "github.com/switchupcb/dbgo/cmd/dbgo_query" +) + +const ( + subcommandDescriptionSchema = "Generates a schema.sql and schema.go file representing your database in the queries directory." +) + +var ( + cmdSchemaFlagSQL = new(bool) + cmdSchemaFlagGo = new(bool) +) + +// cmdSchema represents the dbgo query schema command. +var cmdSchema = &cobra.Command{ + Use: "schema", + Short: "Generate a schema.sql and schema.go file in your queries directory.", + Long: subcommandDescriptionSchema, + Run: func(cmd *cobra.Command, args []string) { + // parse the "--yml" flag. + yml, err := parseFlagYML() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) + + os.Exit(constant.OSExitCodeError) + } + + // generate the schema files. + fmt.Println("Generating schema file(s).") + + // Here is the configuration matrix for this command. + // + // --------------------------------------------- + // | SQL | Go | Result | + // --------------------------------------------- + // | false | false | Generate both. | + // | true | true | Generate both. | + // | false | true | Generate schema.go only. | + // | true | false | Generate schema.sql only. | + // |-------------------------------------------| + if *cmdSchemaFlagSQL == *cmdSchemaFlagGo { + *cmdSchemaFlagSQL = true + *cmdSchemaFlagGo = true + } + + if err := query.Schema(*yml, *cmdSchemaFlagGo, *cmdSchemaFlagSQL); err != nil { + fmt.Fprintf(os.Stderr, "%v\n\n", fmt.Errorf("%w", err)) + + os.Exit(constant.OSExitCodeError) + } + + fmt.Println("\nGenerated schema file(s).") + }, +} + +func init() { + cmdQuery.AddCommand(cmdSchema) + + cmdSchemaFlagSQL = cmdSchema.Flags().BoolP("sql", "s", false, "Use --sql to only generate a schema.sql file.") + cmdSchemaFlagGo = cmdSchema.Flags().BoolP("go", "g", false, "Use --go to only generate a schema.go file.") +} diff --git a/cmd/dbgo_query_template.go b/cmd/dbgo_query_template.go index 157423e..e704c87 100644 --- a/cmd/dbgo_query_template.go +++ b/cmd/dbgo_query_template.go @@ -6,49 +6,41 @@ import ( "path/filepath" "github.com/spf13/cobra" + "github.com/switchupcb/dbgo/cmd/constant" query "github.com/switchupcb/dbgo/cmd/dbgo_query" ) const ( - subcommand_description_template = "Adds a `name` template to the queries `templates` directory. The template contains Go type database models you can use to return a type-safe SQL statement from the `SQL()` function in `name.go` which is called by `db query save`." + subcommandDescriptionTemplate = "Adds a `name` template to the queries `templates` directory. The template contains Go type database models you can use to return a type-safe SQL statement from the `SQL()` function in `name.go` which is called by `db query save`." ) -// templateCmd represents the dbgo query template command. -var templateCmd = &cobra.Command{ +// cmdTemplate represents the dbgo query template command. +var cmdTemplate = &cobra.Command{ Use: "template", Short: "Add an SQL generator template to your queries directory.", - Long: subcommand_description_template, + Long: subcommandDescriptionTemplate, Run: func(cmd *cobra.Command, args []string) { // parse the "--yml" flag. - yml, err := parseYML() + yml, err := parseFlagYML() if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } - // run the generator for each template. + // add each template to the filepath arguments when no filepath arguments are provided. if len(args) == 0 { - queriesGoTemplatesDir := filepath.Join(yml.Generated.Input.Queries, queriesGoTemplatesDirname) + queriesGoTemplatesDir := filepath.Join(yml.Generated.Input.Queries, constant.DirnameQueriesTemplates) files, err := os.ReadDir(queriesGoTemplatesDir) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", fmt.Errorf("%w", err)) - os.Exit(1) + os.Exit(constant.OSExitCodeError) } for i := range files { - output, err := query.Template(filepath.Join(queriesGoTemplatesDir, files[i].Name()), *yml) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n\n", fmt.Errorf("%w", err)) - - continue - } - - fmt.Printf("%v\n\n", output) + args = append(args, filepath.Join(queriesGoTemplatesDir, files[i].Name())) } - - return } // run the generator for each filepath argument. @@ -60,18 +52,27 @@ var templateCmd = &cobra.Command{ continue } - output, err := query.Template(abspath, *yml) - if err != nil { + templateName := filepath.Base(abspath) + + templateDirpath := filepath.Join( + yml.Generated.Input.Queries, // queries + constant.DirnameQueriesTemplates, // templates + templateName, // template (name) + ) + + fmt.Printf("UPDATING TEMPLATE %q at %v\n", templateName, templateDirpath) + + if err := query.Template(templateName, *yml); err != nil { fmt.Fprintf(os.Stderr, "%v\n\n", fmt.Errorf("%w", err)) continue } - fmt.Printf("%v\n\n", output) + fmt.Printf("UPDATED TEMPLATE %q at %v\n\n", templateName, templateDirpath) } }, } func init() { - cmdQuery.AddCommand(templateCmd) + cmdQuery.AddCommand(cmdTemplate) } diff --git a/cmd/flag.go b/cmd/flag.go new file mode 100644 index 0000000..970663c --- /dev/null +++ b/cmd/flag.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/switchupcb/dbgo/cmd/config" +) + +const ( + flagYMLName = "yml" + flagYMLShorthand = "y" + flagYMLUsage = `The path to the .yml file used for code generation (from the current working directory).` + flagYMLUsageErrorUnspecified = "you must specify a .yml configuration file using --yml path/to/yml" +) + +var ( + flagYML = new(string) +) + +// parseFlagYML parses a "--yml" flag. +func parseFlagYML() (*config.YML, error) { + if flagYML == nil || *flagYML == "" { + return nil, errors.New(flagYMLUsageErrorUnspecified) + } + + // The configuration file is loaded (.yml) + yml, err := config.LoadYML(*flagYML) + if err != nil { + return nil, fmt.Errorf("error loading config: %w", err) + } + + return yml, nil +} + +// parseArgFilepath parses a filepath argument to return an absolute filepath to a template directory. +func parseArgFilepath(unknownpath string) (string, error) { + if filepath.Ext(unknownpath) != "" { + return "", fmt.Errorf("specified filepath is not a directory: %q", unknownpath) + } + + // determine the actual filepath of the setup file. + if !filepath.IsAbs(unknownpath) { + if unknownpath, err := filepath.Abs(unknownpath); err != nil { + return "", fmt.Errorf("error determining the absolute file path of the specified template: %q\n%w", unknownpath, err) + } + } + + return unknownpath, nil +} diff --git a/examples/go.mod b/examples/go.mod index 6df017b..142eefb 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,3 +1,22 @@ module github.com/switchupcb/dbgo/examples go 1.23.6 + +require ( + github.com/jackc/pgx/v5 v5.7.2 + github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/text v0.23.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 0000000..c0ed3b1 --- /dev/null +++ b/examples/go.sum @@ -0,0 +1,43 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64 h1:kT/KmzMljUcqsZ8eLikfDXe9gUZbXrFbL2KIipT6/BI= +github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64/go.mod h1:CmEw8R+Js0LqFcz+NbMFtu0e52FnthimBVzPrbETxEA= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/main/datastore/psql/combined.sql.go b/examples/main/datastore/psql/combined.sql.go new file mode 100644 index 0000000..ade701a --- /dev/null +++ b/examples/main/datastore/psql/combined.sql.go @@ -0,0 +1,386 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: combined.sql + +package psql + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countAccount = `-- name: CountAccount :one +SELECT count(*) as account_count from accounts +` + +// Count # of Account +func (q *Queries) CountAccount(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countAccount) + var account_count int64 + err := row.Scan(&account_count) + return account_count, err +} + +const countUser = `-- name: CountUser :one +SELECT count(*) as user_count from users +` + +// Count # of User +func (q *Queries) CountUser(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countUser) + var user_count int64 + err := row.Scan(&user_count) + return user_count, err +} + +const deleteAccount = `-- name: DeleteAccount :exec +DELETE FROM accounts +WHERE id = $1 +` + +// Delete one Account using id +func (q *Queries) DeleteAccount(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, deleteAccount, id) + return err +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1 +` + +// Delete one User using id +func (q *Queries) DeleteUser(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, deleteUser, id) + return err +} + +const insertAccount = `-- name: InsertAccount :one +INSERT INTO accounts +( + first_name + , last_name + , email + , created_at + , updated_at +) VALUES ( + $1 + , $2 + , $3 + , $4 + , $5 +) +RETURNING id, first_name, last_name, email, created_at, updated_at +` + +type InsertAccountParams struct { + FirstName pgtype.Text + LastName pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +// Insert one row of Account +func (q *Queries) InsertAccount(ctx context.Context, arg InsertAccountParams) (Account, error) { + row := q.db.QueryRow(ctx, insertAccount, + arg.FirstName, + arg.LastName, + arg.Email, + arg.CreatedAt, + arg.UpdatedAt, + ) + var i Account + err := row.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const insertUser = `-- name: InsertUser :one +INSERT INTO users +( + name + , password + , email + , created_at + , updated_at +) VALUES ( + $1 + , $2 + , $3 + , $4 + , $5 +) +RETURNING id, name, password, email, created_at, updated_at +` + +type InsertUserParams struct { + Name pgtype.Text + Password pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +// Insert one row of User +func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) { + row := q.db.QueryRow(ctx, insertUser, + arg.Name, + arg.Password, + arg.Email, + arg.CreatedAt, + arg.UpdatedAt, + ) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Password, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listAccount = `-- name: ListAccount :many +SELECT id, first_name, last_name, email, created_at, updated_at FROM accounts +WHERE id > $1 +ORDER BY id +LIMIT 1000 +` + +// Lists 1000 Account having id > @id +func (q *Queries) ListAccount(ctx context.Context, id int32) ([]Account, error) { + rows, err := q.db.Query(ctx, listAccount, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listUser = `-- name: ListUser :many +SELECT id, name, password, email, created_at, updated_at FROM users +WHERE id > $1 +ORDER BY id +LIMIT 1000 +` + +// Lists 1000 User having id > @id +func (q *Queries) ListUser(ctx context.Context, id int32) ([]User, error) { + rows, err := q.db.Query(ctx, listUser, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Password, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectAccount = `-- name: SelectAccount :one +SELECT + id + , first_name + , last_name + , email + , created_at + , updated_at +FROM accounts +WHERE id = $1 +` + +// Select one Account using id +func (q *Queries) SelectAccount(ctx context.Context, id int32) (Account, error) { + row := q.db.QueryRow(ctx, selectAccount, id) + var i Account + err := row.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const selectUser = `-- name: SelectUser :one +SELECT + id + , name + , password + , email + , created_at + , updated_at +FROM users +WHERE id = $1 +` + +// Select one User using id +func (q *Queries) SelectUser(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRow(ctx, selectUser, id) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Password, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateAccount = `-- name: UpdateAccount :one +UPDATE accounts +SET + first_name = $1 + , last_name = $2 + , email = $3 + , created_at = $4 + , updated_at = $5 +WHERE id = $6 +RETURNING id, first_name, last_name, email, created_at, updated_at +` + +type UpdateAccountParams struct { + FirstName pgtype.Text + LastName pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + ID int32 +} + +// Update one row of Account using id +func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) { + row := q.db.QueryRow(ctx, updateAccount, + arg.FirstName, + arg.LastName, + arg.Email, + arg.CreatedAt, + arg.UpdatedAt, + arg.ID, + ) + var i Account + err := row.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateUser = `-- name: UpdateUser :one +UPDATE users +SET + name = $1 + , password = $2 + , email = $3 + , created_at = $4 + , updated_at = $5 +WHERE id = $6 +RETURNING id, name, password, email, created_at, updated_at +` + +type UpdateUserParams struct { + Name pgtype.Text + Password pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + ID int32 +} + +// Update one row of User using id +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { + row := q.db.QueryRow(ctx, updateUser, + arg.Name, + arg.Password, + arg.Email, + arg.CreatedAt, + arg.UpdatedAt, + arg.ID, + ) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.Password, + &i.Email, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const example = `-- name: example :exec +SELECT id, first_name, last_name, email, created_at, updated_at FROM accounts +` + +func (q *Queries) example(ctx context.Context) error { + _, err := q.db.Exec(ctx, example) + return err +} + +const name = `-- name: name :exec +SELECT accounts.id AS "accounts.id", + accounts.first_name AS "accounts.first_name", + accounts.last_name AS "accounts.last_name", + accounts.email AS "accounts.email", + accounts.created_at AS "accounts.created_at", + accounts.updated_at AS "accounts.updated_at" +FROM public.accounts +` + +func (q *Queries) name(ctx context.Context) error { + _, err := q.db.Exec(ctx, name) + return err +} diff --git a/examples/main/datastore/psql/db.go b/examples/main/datastore/psql/db.go new file mode 100644 index 0000000..dda5939 --- /dev/null +++ b/examples/main/datastore/psql/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package psql + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/main/datastore/psql/models.go b/examples/main/datastore/psql/models.go new file mode 100644 index 0000000..400cff9 --- /dev/null +++ b/examples/main/datastore/psql/models.go @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package psql + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Account struct { + ID int32 + FirstName pgtype.Text + LastName pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +type User struct { + ID int32 + Name pgtype.Text + Password pgtype.Text + Email string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} diff --git a/examples/main/datastore/psql/queries/_dbgo.sql b/examples/main/datastore/psql/queries/_dbgo.sql new file mode 100644 index 0000000..7cfbd06 --- /dev/null +++ b/examples/main/datastore/psql/queries/_dbgo.sql @@ -0,0 +1,130 @@ +-- name: CountAccount :one +-- Count # of Account +SELECT count(*) as account_count from accounts ; + +-- name: CountUser :one +-- Count # of User +SELECT count(*) as user_count from users ; + +-- name: DeleteAccount :exec +-- Delete one Account using id +DELETE FROM accounts +WHERE id = @id ; + +-- name: DeleteUser :exec +-- Delete one User using id +DELETE FROM users +WHERE id = @id ; + +-- name: InsertAccount :one +-- Insert one row of Account +INSERT INTO accounts +( + first_name + , last_name + , email + , created_at + , updated_at +) VALUES ( + @first_name + , @last_name + , @email + , @created_at + , @updated_at +) +RETURNING * ; + +-- name: InsertUser :one +-- Insert one row of User +INSERT INTO users +( + name + , password + , email + , created_at + , updated_at +) VALUES ( + @name + , @password + , @email + , @created_at + , @updated_at +) +RETURNING * ; + +-- name: ListAccount :many +-- Lists 1000 Account having id > @id +SELECT * FROM accounts +WHERE id > @id +ORDER BY id +LIMIT 1000 ; + +-- name: ListUser :many +-- Lists 1000 User having id > @id +SELECT * FROM users +WHERE id > @id +ORDER BY id +LIMIT 1000 ; + + +-- name: SelectAccount :one +-- Select one Account using id +SELECT + id + , first_name + , last_name + , email + , created_at + , updated_at +FROM accounts +WHERE id = @id ; + +-- name: SelectUser :one +-- Select one User using id +SELECT + id + , name + , password + , email + , created_at + , updated_at +FROM users +WHERE id = @id ; + +-- name: UpdateAccount :one +-- Update one row of Account using id +UPDATE accounts +SET + first_name = @first_name + , last_name = @last_name + , email = @email + , created_at = @created_at + , updated_at = @updated_at +WHERE id = @id +RETURNING * ; + +-- name: UpdateUser :one +-- Update one row of User using id +UPDATE users +SET + name = @name + , password = @password + , email = @email + , created_at = @created_at + , updated_at = @updated_at +WHERE id = @id +RETURNING * ; + +-- name: example :exec +SELECT * FROM accounts; + +-- name: name :exec +SELECT accounts.id AS "accounts.id", + accounts.first_name AS "accounts.first_name", + accounts.last_name AS "accounts.last_name", + accounts.email AS "accounts.email", + accounts.created_at AS "accounts.created_at", + accounts.updated_at AS "accounts.updated_at" +FROM public.accounts; + + diff --git a/examples/main/datastore/psql/queries/schema/schema.go b/examples/main/datastore/psql/queries/schema/schema.go new file mode 100644 index 0000000..7bd60d0 --- /dev/null +++ b/examples/main/datastore/psql/queries/schema/schema.go @@ -0,0 +1,106 @@ +package sql + +import "github.com/switchupcb/jet/v2/postgres" + +var ( + Accounts = newAccountsTable("public", "accounts", "") + Users = newUsersTable("public", "users", "") +) + +type ( + accountsTable struct { + postgres.Table + ID postgres.ColumnInteger + FirstName postgres.ColumnString + LastName postgres.ColumnString + Email postgres.ColumnString + CreatedAt postgres.ColumnTimestamp + UpdatedAt postgres.ColumnTimestamp + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList + } + AccountsTable struct { + accountsTable + EXCLUDED accountsTable + } + usersTable struct { + postgres.Table + ID postgres.ColumnInteger + Name postgres.ColumnString + Password postgres.ColumnString + Email postgres.ColumnString + CreatedAt postgres.ColumnTimestamp + UpdatedAt postgres.ColumnTimestamp + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList + } + UsersTable struct { + usersTable + EXCLUDED usersTable + } +) + +func UseSchema(schema string) { + Accounts = Accounts.FromSchema(schema) + Users = Users.FromSchema(schema) +} +func (a UsersTable) AS(alias string) *UsersTable { + return newUsersTable(a.SchemaName(), a.TableName(), alias) +} +func (a UsersTable) FromSchema(schemaName string) *UsersTable { + return newUsersTable(schemaName, a.TableName(), a.Alias()) +} +func (a UsersTable) WithPrefix(prefix string) *UsersTable { + return newUsersTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} +func (a UsersTable) WithSuffix(suffix string) *UsersTable { + return newUsersTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} +func newUsersTable(schemaName, tableName, alias string) *UsersTable { + return &UsersTable{usersTable: newUsersTableImpl(schemaName, tableName, alias), EXCLUDED: newUsersTableImpl("", "excluded", "")} +} +func newUsersTableImpl(schemaName, tableName, alias string) usersTable { + var ( + IDColumn = postgres.IntegerColumn("id") + NameColumn = postgres.StringColumn("name") + PasswordColumn = postgres.StringColumn("password") + EmailColumn = postgres.StringColumn("email") + CreatedAtColumn = postgres.TimestampColumn("created_at") + UpdatedAtColumn = postgres.TimestampColumn("updated_at") + allColumns = postgres.ColumnList{IDColumn, NameColumn, PasswordColumn, EmailColumn, CreatedAtColumn, UpdatedAtColumn} + mutableColumns = postgres.ColumnList{NameColumn, PasswordColumn, EmailColumn, CreatedAtColumn, UpdatedAtColumn} + defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn} + ) + return usersTable{Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), ID: IDColumn, Name: NameColumn, Password: PasswordColumn, Email: EmailColumn, CreatedAt: CreatedAtColumn, UpdatedAt: UpdatedAtColumn, AllColumns: allColumns, MutableColumns: mutableColumns, DefaultColumns: defaultColumns} +} +func (a AccountsTable) AS(alias string) *AccountsTable { + return newAccountsTable(a.SchemaName(), a.TableName(), alias) +} +func (a AccountsTable) FromSchema(schemaName string) *AccountsTable { + return newAccountsTable(schemaName, a.TableName(), a.Alias()) +} +func (a AccountsTable) WithPrefix(prefix string) *AccountsTable { + return newAccountsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} +func (a AccountsTable) WithSuffix(suffix string) *AccountsTable { + return newAccountsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} +func newAccountsTable(schemaName, tableName, alias string) *AccountsTable { + return &AccountsTable{accountsTable: newAccountsTableImpl(schemaName, tableName, alias), EXCLUDED: newAccountsTableImpl("", "excluded", "")} +} +func newAccountsTableImpl(schemaName, tableName, alias string) accountsTable { + var ( + IDColumn = postgres.IntegerColumn("id") + FirstNameColumn = postgres.StringColumn("first_name") + LastNameColumn = postgres.StringColumn("last_name") + EmailColumn = postgres.StringColumn("email") + CreatedAtColumn = postgres.TimestampColumn("created_at") + UpdatedAtColumn = postgres.TimestampColumn("updated_at") + allColumns = postgres.ColumnList{IDColumn, FirstNameColumn, LastNameColumn, EmailColumn, CreatedAtColumn, UpdatedAtColumn} + mutableColumns = postgres.ColumnList{FirstNameColumn, LastNameColumn, EmailColumn, CreatedAtColumn, UpdatedAtColumn} + defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn} + ) + return accountsTable{Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), ID: IDColumn, FirstName: FirstNameColumn, LastName: LastNameColumn, Email: EmailColumn, CreatedAt: CreatedAtColumn, UpdatedAt: UpdatedAtColumn, AllColumns: allColumns, MutableColumns: mutableColumns, DefaultColumns: defaultColumns} +} diff --git a/examples/main/datastore/psql/queries/schema/schema.sql b/examples/main/datastore/psql/queries/schema/schema.sql new file mode 100644 index 0000000..d4dbcdf --- /dev/null +++ b/examples/main/datastore/psql/queries/schema/schema.sql @@ -0,0 +1,149 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 17.4 +-- Dumped by pg_dump version 17.4 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: pg_database_owner +-- + +CREATE SCHEMA public; + + +ALTER SCHEMA public OWNER TO pg_database_owner; + +-- +-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: pg_database_owner +-- + +COMMENT ON SCHEMA public IS 'standard public schema'; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: accounts; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.accounts ( + id integer NOT NULL, + first_name character varying(255), + last_name character varying(255), + email character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.accounts OWNER TO postgres; + +-- +-- Name: accounts_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.accounts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.accounts_id_seq OWNER TO postgres; + +-- +-- Name: accounts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.accounts_id_seq OWNED BY public.accounts.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + name character varying(255), + password character varying(255), + email character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +ALTER TABLE public.users OWNER TO postgres; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.users_id_seq OWNER TO postgres; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: accounts id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.accounts ALTER COLUMN id SET DEFAULT nextval('public.accounts_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: accounts accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.accounts + ADD CONSTRAINT accounts_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/examples/main/datastore/psql/queries/templates/example/example.go b/examples/main/datastore/psql/queries/templates/example/example.go index 5581bb1..3710a40 100644 --- a/examples/main/datastore/psql/queries/templates/example/example.go +++ b/examples/main/datastore/psql/queries/templates/example/example.go @@ -1,4 +1,4 @@ -package example +package sql // SQL returns return an SQL statement. // diff --git a/examples/main/datastore/psql/queries/templates/example/schema.go b/examples/main/datastore/psql/queries/templates/example/schema.go index 1a0b271..7bd60d0 100644 --- a/examples/main/datastore/psql/queries/templates/example/schema.go +++ b/examples/main/datastore/psql/queries/templates/example/schema.go @@ -1,4 +1,4 @@ -package example +package sql import "github.com/switchupcb/jet/v2/postgres" diff --git a/examples/main/datastore/psql/queries/templates/name/name.go b/examples/main/datastore/psql/queries/templates/name/name.go index 7ae84a8..77f2bce 100644 --- a/examples/main/datastore/psql/queries/templates/name/name.go +++ b/examples/main/datastore/psql/queries/templates/name/name.go @@ -1,4 +1,4 @@ -package name +package sql import . "github.com/switchupcb/jet/v2/postgres" diff --git a/examples/main/datastore/psql/queries/templates/name/schema.go b/examples/main/datastore/psql/queries/templates/name/schema.go index a2d698d..7bd60d0 100644 --- a/examples/main/datastore/psql/queries/templates/name/schema.go +++ b/examples/main/datastore/psql/queries/templates/name/schema.go @@ -1,4 +1,4 @@ -package name +package sql import "github.com/switchupcb/jet/v2/postgres" @@ -8,10 +8,6 @@ var ( ) type ( - UsersTable struct { - usersTable - EXCLUDED usersTable - } accountsTable struct { postgres.Table ID postgres.ColumnInteger @@ -40,6 +36,10 @@ type ( MutableColumns postgres.ColumnList DefaultColumns postgres.ColumnList } + UsersTable struct { + usersTable + EXCLUDED usersTable + } ) func UseSchema(schema string) { diff --git a/go.mod b/go.mod index f6c1f78..582e797 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,9 @@ require ( github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64 + github.com/switchupcb/xstruct v1.0.3-0.20250310191923-994b96e9c6f9 github.com/traefik/yaegi v0.16.1 + golang.org/x/tools v0.31.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -25,10 +27,13 @@ require ( ) require ( + github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgtype v1.14.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index bf3d533..f32521d 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,10 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg= +github.com/dave/jennifer v1.5.0/go.mod h1:4MnyiFIlZS3l5tSDn8VnzE6ffAhYBMB2SZntBsZGUok= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -98,6 +102,8 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -123,6 +129,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64 h1:kT/KmzMljUcqsZ8eLikfDXe9gUZbXrFbL2KIipT6/BI= github.com/switchupcb/jet/v2 v2.12.1-0.20250307035944-26ee4c529d64/go.mod h1:CmEw8R+Js0LqFcz+NbMFtu0e52FnthimBVzPrbETxEA= +github.com/switchupcb/xstruct v1.0.3-0.20250310191923-994b96e9c6f9 h1:vPQdfLO60Fi8YQ2n6bOkw2TR6hdljGv3tW+bRAqnPGQ= +github.com/switchupcb/xstruct v1.0.3-0.20250310191923-994b96e9c6f9/go.mod h1:SeUjzs5jJffh4LBjaE0z2KrFCc7sDQUP/C42jeVtlV8= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -157,6 +165,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -169,6 +179,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -214,6 +226,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=