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=