PostgreSQL for Bjolang: bjosql over Npgsql, for bjoroutines
Every operation suspends its fiber while it waits for the server, and the ambient cancellation token cancels the query. A Db is a pool; a transaction holds one connection and hands its function a Db bound to it, and a nested one is a savepoint. Each SqlValue is a typed parameter, and a value is read back by the PostgreSQL type of its column. The server's errors are DbError with its SQLSTATE, and a lost connection is DbError 08000. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
commit
b7e9c8076f
8 changed files with 1391 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.bjo/
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.bjobuild
|
||||
*.runtimeconfig.json
|
||||
*.deps.json
|
||||
*.db
|
||||
373
LICENSE
Normal file
373
LICENSE
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
180
Readme.org
Normal file
180
Readme.org
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
* bjopg — PostgreSQL for Bjolang
|
||||
|
||||
The [[https://github.com/bjoli/bjosql][bjosql]] query language, running on PostgreSQL through
|
||||
[[https://www.npgsql.org][Npgsql]]. Every operation is a bjoroutine: a query suspends its fiber while it
|
||||
waits for the server, and does not hold a thread.
|
||||
|
||||
#+BEGIN_SRC scheme
|
||||
(import (std datetime))
|
||||
(import (std uuid))
|
||||
(import (bjosql core))
|
||||
(import (bjopg core))
|
||||
|
||||
(def/table aches Ache
|
||||
(id Uuid #:primary-key)
|
||||
(what string)
|
||||
(felt Instant)
|
||||
(severity int))
|
||||
|
||||
(defbjo (main args)
|
||||
(match (call-with-db "Host=localhost;Database=aches"
|
||||
(bjoroutine (db)
|
||||
(def (Ok _) (create-table db aches) :propagate)
|
||||
(def (Ok _) (insert db aches (Ache (id (uuid-v7)) (what "knee") (felt (now)) (severity 2))) :propagate)
|
||||
(fetch db aches '(select (where (severity (>= 2))) (order-by (desc felt))))))
|
||||
((Ok rows) (loop (:for a rows) (:do (println (record-ref a what)))) 0)
|
||||
((Err e) (println (sql-error-message e)) 1)))
|
||||
#+END_SRC
|
||||
|
||||
** Using it
|
||||
|
||||
#+BEGIN_SRC scheme
|
||||
(depends
|
||||
(package (name (bjopg))
|
||||
(version (version-at-least "0.1.0"))
|
||||
(source (git (url "https://rikspucko.koketteriet.se/bjoli/bjopg.git")))))
|
||||
#+END_SRC
|
||||
|
||||
~(bjosql)~ comes along with it. The package declares Npgsql, and ~bjo~
|
||||
restores it for any build that uses the package, so a program declares no NuGet
|
||||
packages of its own.
|
||||
|
||||
** Operations
|
||||
|
||||
Each operation is a bjoroutine, and returns a ~(Result SqlError ...)~.
|
||||
|
||||
| ~(open-db connection-string)~ | a pool of connections; see below |
|
||||
| ~(close-db db)~ | closes the pool and its connections |
|
||||
| ~(call-with-db connection-string f)~ | open, call ~f~, and close however ~f~ returns |
|
||||
| ~(create-table db t)~ | ~CREATE TABLE IF NOT EXISTS~ |
|
||||
| ~(insert db t row)~ | the row as stored, with the key the server chose for an ~#:auto~ column |
|
||||
| ~(insert-all db t rows)~ | all of the rows or none of them, in one transaction |
|
||||
| ~(fetch db t '(select ...))~ | the rows, read into the table's record |
|
||||
| ~(fetch-one db t '(select ...))~ | the first row, as an ~Option~ |
|
||||
| ~(fetch-rows db t '(select ...))~ | the rows as ~Row~ values, for a projection |
|
||||
| ~(execute db t '(update ...))~ | the number of rows changed; also for ~delete~ |
|
||||
| ~(query-sql db s)~ ~(execute-sql db s)~ | SQL written by hand, from ~sql~, with ~?~ for each value |
|
||||
| ~(with-transaction db f)~ | commits when ~f~ returns ~Ok~; rolls back on ~Err~ or on a throw |
|
||||
|
||||
The connection string is Npgsql's: ~"Host=localhost;Port=5432;Database=app;Username=ada;Password=..."~.
|
||||
~open-db~ makes one connection and gives it back, so a server that cannot be
|
||||
reached is an ~Err~ from ~open-db~ and not from the first query. A connection
|
||||
string that cannot be read is the program's mistake, and throws.
|
||||
|
||||
** The pool and transactions
|
||||
|
||||
A ~Db~ is a pool. Each operation takes a connection, runs, and gives it back, so
|
||||
any number of fibers can use one ~Db~ at the same time. Open one ~Db~ when the
|
||||
program starts, and use it everywhere.
|
||||
|
||||
~with-transaction~ holds one connection for its whole function, and gives the
|
||||
function a ~Db~ that runs everything on that connection. Use that ~Db~ inside
|
||||
the function, not the pool: a statement given to the pool runs on another
|
||||
connection, outside the transaction.
|
||||
|
||||
#+BEGIN_SRC scheme
|
||||
(with-transaction db
|
||||
(bjoroutine (tx)
|
||||
(def (Ok _) (execute tx accounts '(update (set (balance ,(- a 100))) (where (id (= 1))))) :propagate)
|
||||
(execute tx accounts '(update (set (balance ,(+ b 100))) (where (id (= 2)))))))
|
||||
#+END_SRC
|
||||
|
||||
A transaction inside a transaction is a savepoint. When it fails, it is rolled
|
||||
back, and the outer transaction goes on. PostgreSQL refuses every statement in
|
||||
a transaction after an error until it is rolled back; the savepoint is what
|
||||
makes the outer transaction usable again.
|
||||
|
||||
The ~Db~ of a transaction is one connection, so use it from one fiber at a time.
|
||||
|
||||
** Fibers and cancellation
|
||||
|
||||
A call suspends the fiber while it waits for the server. The ambient
|
||||
cancellation token is passed to Npgsql, so a cancelled scope cancels its query
|
||||
on the server. With bjoweb's ~#:cancel-on-disconnect #t~, a client that goes
|
||||
away cancels the query it was waiting for.
|
||||
|
||||
There is no blocking version of the operations yet. From code that is not a
|
||||
bjoroutine, use ~(sync/blocking (promise-join (bjo ...)))~.
|
||||
|
||||
** Errors
|
||||
|
||||
~(DbError code message)~ is PostgreSQL's SQLSTATE and message:
|
||||
|
||||
| ~"23505"~ | a unique violation |
|
||||
| ~"23503"~ | a foreign key violation |
|
||||
| ~"42601"~ | SQL the server cannot parse |
|
||||
| ~"3D000"~ | a database that does not exist |
|
||||
| ~"08000"~ | a server that cannot be reached, or a connection that was lost |
|
||||
|
||||
~"08000"~ is connection_exception, the class the SQL standard has for this. A
|
||||
lost connection is a ~DbError~ and not an exception, because a server on the
|
||||
network can go away while the program is correct. ~(DecodeError message)~ is a
|
||||
row that does not fit its record, and ~(BadQuery message)~ is a statement that
|
||||
cannot be written. Anything else is an exception.
|
||||
|
||||
** Types
|
||||
|
||||
| Bjolang | PostgreSQL | Sent as (.NET) |
|
||||
|-----------------+---------------+-------------------------------|
|
||||
| ~int~ | ~integer~ | ~long~ |
|
||||
| ~long~ | ~bigint~ | ~long~ |
|
||||
| ~double~ | ~double precision~ | ~double~ |
|
||||
| ~string~ | ~text~ | ~string~ |
|
||||
| ~bool~ | ~boolean~ | ~bool~ |
|
||||
| ~(Array byte)~ | ~bytea~ | ~byte[]~ |
|
||||
| ~Date~ | ~date~ | ~DateOnly~ |
|
||||
| ~Time~ | ~time~ | ~TimeOnly~ |
|
||||
| ~DateTime~ | ~timestamp~ | ~DateTime~, kind Unspecified |
|
||||
| ~Instant~ | ~timestamptz~ | ~DateTime~, kind Utc |
|
||||
| ~decimal~ | ~numeric~ | ~decimal~ |
|
||||
| ~Uuid~ | ~uuid~ | ~Guid~ |
|
||||
| ~JsonDoc~ | ~jsonb~ | ~string~, as ~jsonb~ |
|
||||
|
||||
Every value is a typed parameter, so no cast is written in the SQL. A value is
|
||||
read back by the PostgreSQL type of its column: a ~date~, a ~timestamp~ and a
|
||||
~timestamptz~ all arrive as a .NET ~DateTime~, and only the type name tells
|
||||
them apart.
|
||||
|
||||
A decimal is ~numeric~, so ~ORDER BY~, ~<~ and ~SUM~ on it are exact and
|
||||
numeric, as they are not in SQLite. ~jsonb~ stores JSON in a form of its own:
|
||||
the value is the same after a read, but the key order and the spaces can
|
||||
differ.
|
||||
|
||||
A type bjosql does not have, such as ~inet~, is a column with ~#:sql-type
|
||||
"INET"~ and a type whose ~ToSql~ answers ~(SqlOther "inet" text)~. It is sent as
|
||||
text with a cast, ~$1::inet~, and the server checks it. A column of a type
|
||||
with no case of its own is read as ~(SqlOther type-name text)~.
|
||||
|
||||
** A server for development
|
||||
|
||||
PostgreSQL does not need systemd. As your own user, without root:
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
initdb -D ~/.local/share/postgres/dev -U "$USER" --auth=trust --encoding=UTF8 --locale=C.UTF-8
|
||||
echo "listen_addresses = 'localhost'" >> ~/.local/share/postgres/dev/postgresql.conf
|
||||
echo "unix_socket_directories = '/tmp'" >> ~/.local/share/postgres/dev/postgresql.conf
|
||||
pg_ctl -D ~/.local/share/postgres/dev -l ~/.local/share/postgres/dev.log start
|
||||
createdb -h localhost bjosql_test
|
||||
#+END_SRC
|
||||
|
||||
~--auth=trust~ lets every local connection in without a password, so the
|
||||
server only listens on localhost. ~pg_ctl ... stop~ stops it. A container that
|
||||
restarts does not start it again: run the ~start~ line.
|
||||
|
||||
** Tests
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
BJOPG_TEST="Host=localhost;Database=bjosql_test" bjo run tests/pg.bjo
|
||||
#+END_SRC
|
||||
|
||||
Every line should start with "ok:". Without ~BJOPG_TEST~ the test says so and
|
||||
runs nothing. It makes tables named ~bjopg_...~, and drops them first and last.
|
||||
|
||||
It checks that each type is stored as PostgreSQL's own type and reads back
|
||||
equal; that ~ORDER BY~ and tests on an instant, a uuid and a decimal are those
|
||||
of the values; nulls in typed columns; the counts of updates and deletes; the
|
||||
SQLSTATE of a unique violation, of bad SQL, of a database that does not exist
|
||||
and of a server that is not there; transactions that commit, roll back on
|
||||
~Err~ and on a throw, and a savepoint that fails inside a transaction that then
|
||||
commits; a type of one's own sent as ~SqlOther~ with a cast; and 20 fibers
|
||||
querying one pool at once, which overlap rather than wait for each other.
|
||||
5
bjo.lock
Normal file
5
bjo.lock
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
(lock
|
||||
(locked
|
||||
(package (name (bjosql)) (version "0.3.0")
|
||||
(source (git (url "https://github.com/bjoli/bjosql")))
|
||||
(commit "733866e091804ea8a6caa445f5ac191715f78545"))))
|
||||
17
manifest.bjodat
Normal file
17
manifest.bjodat
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
(package
|
||||
(name (bjopg))
|
||||
(version "0.1.0")
|
||||
(description "PostgreSQL for Bjolang: the bjosql query language over Npgsql, for bjoroutines.")
|
||||
(license "MPL-2.0")
|
||||
|
||||
(depends
|
||||
(package (name (bjosql))
|
||||
(version (version-at-least "0.3.0"))
|
||||
(source (git (url "https://github.com/bjoli/bjosql")))))
|
||||
|
||||
;; A program using this package declares nothing itself.
|
||||
(packages
|
||||
(nuget (id "Npgsql") (version "10.0.0")))
|
||||
|
||||
;; A library: there is no src/main.bjo.
|
||||
)
|
||||
29
packages.lock.json
Normal file
29
packages.lock.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Npgsql": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.0, )",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
487
src/core.bjo
Normal file
487
src/core.bjo
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
;; bjopg/src/core.bjo — the module (bjopg core).
|
||||
;;
|
||||
;; PostgreSQL, through Npgsql, spoken in (bjosql core)'s language:
|
||||
;;
|
||||
;; (def/table users User
|
||||
;; (id Uuid #:primary-key)
|
||||
;; (name string)
|
||||
;; (joined Instant))
|
||||
;;
|
||||
;; (def (Ok db) (open-db "Host=localhost;Database=app") :propagate)
|
||||
;; (def (Ok _) (create-table db users) :propagate)
|
||||
;; (def (Ok ada) (insert db users (User (id (uuid-v7)) (name "Ada") (joined (now)))) :propagate)
|
||||
;; (fetch db users '(select (where (name (like "A%"))) (order-by joined)))
|
||||
;;
|
||||
;; Every operation is a bjoroutine. A query suspends the fiber while it waits
|
||||
;; for the server, and the ambient cancellation token cancels it on the server.
|
||||
;;
|
||||
;; A `Db` is a pool of connections. Each operation takes a connection from the
|
||||
;; pool and gives it back. `with-transaction` holds one connection, and gives
|
||||
;; its function a `Db` that uses that connection, so the operations are the
|
||||
;; same inside a transaction and outside one.
|
||||
;;
|
||||
;; Every operation answers a `Result` with a `SqlError`: what the server
|
||||
;; refused, a server that cannot be reached, or a row that does not fit its
|
||||
;; record. Anything else is an exception, as it is everywhere.
|
||||
|
||||
(export
|
||||
Db open-db close-db call-with-db
|
||||
postgres-dialect
|
||||
|
||||
;; With a table
|
||||
create-table insert insert-all fetch fetch-one fetch-rows execute
|
||||
|
||||
;; By hand
|
||||
execute-sql query-sql
|
||||
|
||||
with-transaction)
|
||||
|
||||
(import (std prelude))
|
||||
(import (only (std datetime)
|
||||
date time datetime ->year ->month ->day ->hours ->minutes ->seconds ->nanoseconds
|
||||
instant->utc-datetime utc-datetime->instant))
|
||||
(import (only (std uuid) uuid->guid guid->uuid))
|
||||
(import (bjosql core))
|
||||
|
||||
(import/class
|
||||
(Object (: System.Object))
|
||||
(Exception (: System.Exception))
|
||||
(DataSource (: Npgsql.NpgsqlDataSource))
|
||||
(Connection (: Npgsql.NpgsqlConnection))
|
||||
(Command (: Npgsql.NpgsqlCommand))
|
||||
(Parameters (: Npgsql.NpgsqlParameterCollection))
|
||||
(Parameter (: Npgsql.NpgsqlParameter (-> Parameter)))
|
||||
(Reader (: Npgsql.NpgsqlDataReader))
|
||||
(PostgresException (: Npgsql.PostgresException))
|
||||
(NpgsqlDbType (: NpgsqlTypes.NpgsqlDbType))
|
||||
(DateTimeKind (: System.DateTimeKind))
|
||||
(NetDateTime (: System.DateTime (-> int int int int int int DateTimeKind NetDateTime)))
|
||||
(DateOnly (: System.DateOnly (-> int int int DateOnly)))
|
||||
(TimeOnly (: System.TimeOnly (-> long TimeOnly)))
|
||||
(TimeSpan (: System.TimeSpan))
|
||||
(Guid (: System.Guid)))
|
||||
|
||||
(import/extern
|
||||
(data-source-create (: Npgsql.NpgsqlDataSource.Create (-> string DataSource)))
|
||||
(data-source-dispose (: Npgsql.NpgsqlDataSource.Dispose (-> DataSource void)))
|
||||
(open-connection (: Npgsql.NpgsqlDataSource.OpenConnectionAsync (-> DataSource Connection) #:async))
|
||||
(connection-dispose (: Npgsql.NpgsqlConnection.Dispose (-> Connection void)))
|
||||
(connection-command (: Npgsql.NpgsqlConnection.CreateCommand (-> Connection Command)))
|
||||
|
||||
(command-text! (: Npgsql.NpgsqlCommand.CommandText (-> Command string void) #:set))
|
||||
(command-parameters (: Npgsql.NpgsqlCommand.Parameters (-> Command Parameters) #:get))
|
||||
(command-execute (: Npgsql.NpgsqlCommand.ExecuteNonQueryAsync (-> Command int) #:async))
|
||||
(command-reader (: Npgsql.NpgsqlCommand.ExecuteReaderAsync (-> Command Reader) #:async))
|
||||
(command-dispose (: Npgsql.NpgsqlCommand.Dispose (-> Command void)))
|
||||
|
||||
(parameters-add (: Npgsql.NpgsqlParameterCollection.Add (-> Parameters Parameter Parameter)))
|
||||
(parameter-value! (: Npgsql.NpgsqlParameter.Value (-> Parameter Object void) #:set))
|
||||
(parameter-type! (: Npgsql.NpgsqlParameter.NpgsqlDbType (-> Parameter NpgsqlDbType void) #:set))
|
||||
(db-null (: System.DBNull.Value #:get))
|
||||
|
||||
(reader-read (: Npgsql.NpgsqlDataReader.ReadAsync (-> Reader bool) #:async))
|
||||
(reader-field-count (: Npgsql.NpgsqlDataReader.FieldCount (-> Reader int) #:get))
|
||||
(reader-name (: Npgsql.NpgsqlDataReader.GetName (-> Reader int string)))
|
||||
(reader-null? (: Npgsql.NpgsqlDataReader.IsDBNull (-> Reader int bool)))
|
||||
(reader-type-name (: Npgsql.NpgsqlDataReader.GetDataTypeName (-> Reader int string)))
|
||||
(reader-long (: Npgsql.NpgsqlDataReader.GetInt64 (-> Reader int long)))
|
||||
(reader-double (: Npgsql.NpgsqlDataReader.GetDouble (-> Reader int double)))
|
||||
(reader-decimal (: Npgsql.NpgsqlDataReader.GetDecimal (-> Reader int decimal)))
|
||||
(reader-bool (: Npgsql.NpgsqlDataReader.GetBoolean (-> Reader int bool)))
|
||||
(reader-string (: Npgsql.NpgsqlDataReader.GetString (-> Reader int string)))
|
||||
(reader-datetime (: Npgsql.NpgsqlDataReader.GetDateTime (-> Reader int NetDateTime)))
|
||||
(reader-timespan (: Npgsql.NpgsqlDataReader.GetTimeSpan (-> Reader int TimeSpan)))
|
||||
(reader-guid (: Npgsql.NpgsqlDataReader.GetGuid (-> Reader int Guid)))
|
||||
(reader-value (: Npgsql.NpgsqlDataReader.GetValue (-> Reader int Object)))
|
||||
(reader-dispose (: Npgsql.NpgsqlDataReader.Dispose (-> Reader void)))
|
||||
;; `Convert.ToString` and not `Object.ToString`: a value can be null, as a
|
||||
;; `void` column's is.
|
||||
(object-text (: System.Convert.ToString (-> Object string)))
|
||||
|
||||
(net-year (: System.DateTime.Year (-> NetDateTime int) #:get))
|
||||
(net-month (: System.DateTime.Month (-> NetDateTime int) #:get))
|
||||
(net-day (: System.DateTime.Day (-> NetDateTime int) #:get))
|
||||
(net-hour (: System.DateTime.Hour (-> NetDateTime int) #:get))
|
||||
(net-minute (: System.DateTime.Minute (-> NetDateTime int) #:get))
|
||||
(net-second (: System.DateTime.Second (-> NetDateTime int) #:get))
|
||||
(net-ticks (: System.DateTime.Ticks (-> NetDateTime long) #:get))
|
||||
(net-add-ticks (: System.DateTime.AddTicks (-> NetDateTime long NetDateTime)))
|
||||
(span-ticks (: System.TimeSpan.Ticks (-> TimeSpan long) #:get))
|
||||
|
||||
(exception-message (: System.Exception.Message (-> Exception string) #:get))
|
||||
(sql-state (: Npgsql.PostgresException.SqlState (-> PostgresException string) #:get)))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; The dialect
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(: postgres-type-name (-> ColumnType string))
|
||||
(defun (postgres-type-name t)
|
||||
(match t
|
||||
((IntegerType) "BIGINT")
|
||||
((Int32Type) "INTEGER")
|
||||
((RealType) "DOUBLE PRECISION")
|
||||
((TextType) "TEXT")
|
||||
((BlobType) "BYTEA")
|
||||
((BoolType) "BOOLEAN")
|
||||
((DateType) "DATE")
|
||||
((TimeType) "TIME")
|
||||
((TimestampType) "TIMESTAMP")
|
||||
((InstantType) "TIMESTAMPTZ")
|
||||
((DecimalType) "NUMERIC")
|
||||
((UuidType) "UUID")
|
||||
((JsonType) "JSONB")
|
||||
((CustomType name) name)))
|
||||
|
||||
;; `$1`, `$2`: Npgsql passes positional parameters to the server as they are,
|
||||
;; when the parameters have no names. `SqlOther` gets a cast to its type.
|
||||
(: postgres-dialect Dialect)
|
||||
(def postgres-dialect
|
||||
(record-set standard-dialect
|
||||
(placeholder (fun (n) (str "$" (int->string n))))
|
||||
(type-name postgres-type-name)
|
||||
(param-cast postgres-style-cast)))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; The pool
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; The pool, and the connection of the transaction it is in, if it is in one.
|
||||
;; `depth` counts the transactions open on that connection, which names their
|
||||
;; savepoints.
|
||||
(type (: Db #:opaque (Record (: source DataSource)
|
||||
(: connection (Option Connection))
|
||||
(: depth int))))
|
||||
|
||||
;; What went wrong, as a `SqlError`. A `PostgresException` is the server's
|
||||
;; answer, with its SQLSTATE. Any other `NpgsqlException` is a connection that
|
||||
;; could not be made or was lost; it is given SQLSTATE 08000,
|
||||
;; connection_exception, the class the standard has for it.
|
||||
(: failed (-> Exception SqlError))
|
||||
(defun (failed e)
|
||||
(match e
|
||||
((:is Npgsql.PostgresException pe) (DbError (sql-state pe) (exception-message e)))
|
||||
(_ (DbError "08000" (exception-message e)))))
|
||||
|
||||
(: pg (-bjo-> (-bjo-> %a) (Result SqlError %a)))
|
||||
(defbjo (pg thunk)
|
||||
(match (try (thunk) #:catch (Npgsql.NpgsqlException))
|
||||
((Ok x) (Ok x))
|
||||
((Err e) (Err (failed e)))))
|
||||
|
||||
;; A pool for the server that `connection-string` names, such as
|
||||
;; `"Host=localhost;Database=app;Username=ada"`. A connection is made, and
|
||||
;; given back, so that a server that cannot be reached is an `Err` here and not
|
||||
;; at the first query. A connection string that cannot be read is the
|
||||
;; program's mistake, and throws.
|
||||
(: open-db (-bjo-> string (Result SqlError Db)))
|
||||
(defbjo (open-db connection-string)
|
||||
(def source (data-source-create connection-string))
|
||||
(match (pg (bjoroutine () (connection-dispose (open-connection source))))
|
||||
((Ok _) (Ok (Db (source source) (connection None) (depth 0))))
|
||||
((Err e)
|
||||
(data-source-dispose source)
|
||||
(Err e))))
|
||||
|
||||
;; Closes the pool, and every connection in it.
|
||||
(: close-db (-> Db void))
|
||||
(defun (close-db db) (data-source-dispose (record-ref db source)))
|
||||
|
||||
;; Opens a pool, hands it to `f`, and closes it when `f` returns, however it
|
||||
;; returns.
|
||||
(: call-with-db (-bjo-> string (-bjo-> Db (Result SqlError %a)) (Result SqlError %a)))
|
||||
(defbjo (call-with-db connection-string f)
|
||||
(match (open-db connection-string)
|
||||
((Err e) (Err e))
|
||||
((Ok db) (try (f db) #:finally (close-db db)))))
|
||||
|
||||
;; `f` with the transaction's connection, or with one from the pool, given
|
||||
;; back when `f` returns.
|
||||
(: with-connection (-bjo-> Db (-bjo-> Connection %a) %a))
|
||||
(defbjo (with-connection db f)
|
||||
(match (record-ref db connection)
|
||||
((Some c) (f c))
|
||||
(None
|
||||
(def c (open-connection (record-ref db source)))
|
||||
(try (f c) #:finally (connection-dispose c)))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Values
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(: boxed (-> %a Object))
|
||||
(defun (boxed x) (cast System.Object x))
|
||||
|
||||
(: long-of (-> int long))
|
||||
(defun (long-of n) (cast long n))
|
||||
|
||||
;; A Bjolang time of day as .NET ticks of 100 ns. Values are kept to the
|
||||
;; microsecond, so nothing is lost.
|
||||
(: day-ticks (-> int int int int long))
|
||||
(defun (day-ticks h m s nanoseconds)
|
||||
(+ (* (+ (* (+ (* (long-of h) 60L) (long-of m)) 60L) (long-of s)) 10000000L)
|
||||
(long-of (/ nanoseconds 100))))
|
||||
|
||||
(: net-datetime (-> DateTime DateTimeKind NetDateTime))
|
||||
(defun (net-datetime t kind)
|
||||
(net-add-ticks (NetDateTime. (->year t) (->month t) (->day t) (->hours t) (->minutes t) (->seconds t) kind)
|
||||
(long-of (/ (->nanoseconds t) 100))))
|
||||
|
||||
;; Each value as the .NET type Npgsql sends as the PostgreSQL type of its
|
||||
;; column. Npgsql sends a `DateTime` of kind Utc as `timestamptz`, and of kind
|
||||
;; Unspecified as `timestamp`. JSON text has to be told it is `jsonb`, or it is
|
||||
;; sent as `text`. `SqlOther` is sent as text, and the SQL casts it.
|
||||
(: bind! (-> Parameters SqlValue void))
|
||||
(defun (bind! ps v)
|
||||
(def p (Parameter.))
|
||||
(match v
|
||||
((SqlNull) (parameter-value! p (boxed db-null)))
|
||||
((SqlInt n) (parameter-value! p (boxed n)))
|
||||
((SqlReal d) (parameter-value! p (boxed d)))
|
||||
((SqlText s) (parameter-value! p (boxed (string->clr-string s))))
|
||||
((SqlBlob b) (parameter-value! p (boxed b)))
|
||||
((SqlBool b) (parameter-value! p (boxed b)))
|
||||
((SqlDate d) (parameter-value! p (boxed (DateOnly. (->year d) (->month d) (->day d)))))
|
||||
((SqlTime t)
|
||||
(parameter-value! p (boxed (TimeOnly. (day-ticks (->hours t) (->minutes t) (->seconds t) (->nanoseconds t))))))
|
||||
((SqlTimestamp t) (parameter-value! p (boxed (net-datetime t DateTimeKind.Unspecified))))
|
||||
((SqlInstant i) (parameter-value! p (boxed (net-datetime (instant->utc-datetime i) DateTimeKind.Utc))))
|
||||
((SqlDecimal d) (parameter-value! p (boxed d)))
|
||||
((SqlUuid u) (parameter-value! p (boxed (uuid->guid u))))
|
||||
((SqlJson s)
|
||||
(parameter-value! p (boxed (string->clr-string s)))
|
||||
(parameter-type! p NpgsqlDbType.Jsonb))
|
||||
((SqlOther _ s) (parameter-value! p (boxed (string->clr-string s)))))
|
||||
(ignore (parameters-add ps p)))
|
||||
|
||||
(: some! (-> string (Option %a) %a))
|
||||
(defun (some! what o)
|
||||
(match o
|
||||
((Some x) x)
|
||||
(None (panic! (str "bjopg: PostgreSQL answered " what " that Bjolang cannot hold")))))
|
||||
|
||||
;; The fraction of a second in a .NET tick count, in nanoseconds.
|
||||
(: ticks-nanoseconds (-> long int))
|
||||
(defun (ticks-nanoseconds ticks) (cast int (* (% ticks 10000000L) 100L)))
|
||||
|
||||
(: datetime-of (-> NetDateTime DateTime))
|
||||
(defun (datetime-of t)
|
||||
(some! "a timestamp"
|
||||
(datetime (net-year t) (net-month t) (net-day t)
|
||||
#:hours (net-hour t) #:minutes (net-minute t) #:seconds (net-second t)
|
||||
#:nanoseconds (ticks-nanoseconds (net-ticks t)))))
|
||||
|
||||
(: time-of (-> TimeSpan Time))
|
||||
(defun (time-of span)
|
||||
(def ticks (span-ticks span))
|
||||
(def seconds (/ ticks 10000000L))
|
||||
(some! "a time"
|
||||
(time (cast int (/ seconds 3600L)) (cast int (% (/ seconds 60L) 60L))
|
||||
#:seconds (cast int (% seconds 60L))
|
||||
#:nanoseconds (ticks-nanoseconds ticks))))
|
||||
|
||||
;; `character varying(80)` is `character varying`.
|
||||
(: base-type-name (-> string string))
|
||||
(defun (base-type-name name)
|
||||
(string-trim (vec-ref (string-split name "(") 0)))
|
||||
|
||||
;; A value as the server sent it, by the PostgreSQL type of its column: a
|
||||
;; `date`, a `timestamp` and a `timestamptz` all arrive as a .NET `DateTime`,
|
||||
;; so the .NET type cannot tell them apart. A type with no case of its own is
|
||||
;; `SqlOther`, with the text .NET gives the value.
|
||||
(: read-value (-> Reader int SqlValue))
|
||||
(defun (read-value r i)
|
||||
(if (reader-null? r i)
|
||||
SqlNull
|
||||
(let ((name (base-type-name (reader-type-name r i))))
|
||||
(cond
|
||||
((or (= name "smallint") (= name "integer") (= name "bigint")) (SqlInt (reader-long r i)))
|
||||
((or (= name "real") (= name "double precision")) (SqlReal (reader-double r i)))
|
||||
((= name "numeric") (SqlDecimal (reader-decimal r i)))
|
||||
((= name "boolean") (SqlBool (reader-bool r i)))
|
||||
((= name "bytea") (SqlBlob (cast (Array byte) (reader-value r i))))
|
||||
((or (= name "text") (= name "character varying") (= name "character") (= name "name"))
|
||||
(SqlText (reader-string r i)))
|
||||
((= name "date")
|
||||
(def t (reader-datetime r i))
|
||||
(SqlDate (some! "a date" (date (net-year t) (net-month t) (net-day t)))))
|
||||
((= name "time without time zone") (SqlTime (time-of (reader-timespan r i))))
|
||||
((= name "timestamp without time zone") (SqlTimestamp (datetime-of (reader-datetime r i))))
|
||||
((= name "timestamp with time zone")
|
||||
(SqlInstant (utc-datetime->instant (datetime-of (reader-datetime r i)))))
|
||||
((= name "uuid") (SqlUuid (guid->uuid (reader-guid r i))))
|
||||
((or (= name "json") (= name "jsonb")) (SqlJson (reader-string r i)))
|
||||
;; What a function such as `pg_sleep` answers.
|
||||
((= name "void") SqlNull)
|
||||
(else (SqlOther name (object-text (reader-value r i))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Running statements
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; A command for `s` on `c`, with its parameters bound, handed to `f` and
|
||||
;; disposed of afterwards.
|
||||
(: with-command (-bjo-> Connection Sql (-bjo-> Command %a) %a))
|
||||
(defbjo (with-command c s f)
|
||||
(def st (render postgres-dialect s))
|
||||
(def cmd (connection-command c))
|
||||
(try
|
||||
(let ()
|
||||
(command-text! cmd (statement-text st))
|
||||
(def ps (command-parameters cmd))
|
||||
(loop (:for v (statement-params st))
|
||||
(:do (bind! ps v)))
|
||||
(f cmd))
|
||||
#:finally (command-dispose cmd)))
|
||||
|
||||
(: read-rows (-bjo-> Reader (List Row)))
|
||||
(defbjo (read-rows r)
|
||||
(def count (reader-field-count r))
|
||||
(def names (loop (:for i (range 0 count)) (:acc out (vecing (reader-name r i))) => out))
|
||||
(let go ((out Nil))
|
||||
(if (reader-read r)
|
||||
(go (Cons (make-row names (loop (:for i (range 0 count))
|
||||
(:acc vs (vecing (read-value r i)))
|
||||
=> vs))
|
||||
out))
|
||||
(list-reverse out))))
|
||||
|
||||
;; Runs a statement that answers no rows, and answers how many it changed.
|
||||
(: execute-sql (-bjo-> Db Sql (Result SqlError int)))
|
||||
(defbjo (execute-sql db s)
|
||||
(pg (bjoroutine ()
|
||||
(with-connection db (bjoroutine (c)
|
||||
(with-command c s (bjoroutine (cmd) (command-execute cmd))))))))
|
||||
|
||||
;; Runs a query, and answers its rows.
|
||||
(: query-sql (-bjo-> Db Sql (Result SqlError (List Row))))
|
||||
(defbjo (query-sql db s)
|
||||
(pg (bjoroutine ()
|
||||
(with-connection db (bjoroutine (c)
|
||||
(with-command c s (bjoroutine (cmd)
|
||||
(def r (command-reader cmd))
|
||||
(try (read-rows r) #:finally (reader-dispose r)))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; With a table
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(: create-table (-bjo-> Db (Table %row %col %test %set) (Result SqlError void)))
|
||||
(defbjo (create-table db table)
|
||||
(match (execute-sql db (create-table->sql table))
|
||||
((Ok _) (Ok unit))
|
||||
((Err e) (Err e))))
|
||||
|
||||
;; Inserts a row, and answers it as stored: with the key the server chose for
|
||||
;; an `#:auto` column, whatever the record said.
|
||||
(: insert (-bjo-> Db (Table %row %col %test %set) %row (Result SqlError %row)))
|
||||
(defbjo (insert db table row)
|
||||
(match (query-sql db (insert->sql table row))
|
||||
((Ok (Cons stored _)) (decode-row table stored))
|
||||
((Ok Nil) (Err (DecodeError (str "inserting into " (table-name table) " returned no row."))))
|
||||
((Err e) (Err e))))
|
||||
|
||||
;; Every row or none: in one transaction.
|
||||
(: insert-all (-bjo-> Db (Table %row %col %test %set) (List %row) (Result SqlError (List %row))))
|
||||
(defbjo (insert-all db table rows)
|
||||
(with-transaction db
|
||||
(bjoroutine (tx)
|
||||
(let go ((left rows) (out Nil))
|
||||
(match left
|
||||
(Nil (Ok (list-reverse out)))
|
||||
((Cons row more)
|
||||
(match (insert tx table row)
|
||||
((Ok stored) (go more (Cons stored out)))
|
||||
((Err e) (Err e)))))))))
|
||||
|
||||
;; The rows a query selects, read into the table's record. A projection has
|
||||
;; to name every column for that; `fetch-rows` reads any.
|
||||
(: fetch (-bjo-> Db (Table %row %col %test %set) (Select %col %test) (Result SqlError (List %row))))
|
||||
(defbjo (fetch db table q)
|
||||
(match (query-sql db (select->sql table q))
|
||||
((Ok rows) (decode-rows table rows))
|
||||
((Err e) (Err e))))
|
||||
|
||||
;; The first row, if there is one. Asks for one, whatever the query's limit.
|
||||
(: fetch-one (-bjo-> Db (Table %row %col %test %set) (Select %col %test) (Result SqlError (Option %row))))
|
||||
(defbjo (fetch-one db table q)
|
||||
(def (SelectQuery clauses) q)
|
||||
(match (fetch db table (SelectQuery (list-append clauses (list (Limit 1)))))
|
||||
((Ok (Cons row _)) (Ok (Some row)))
|
||||
((Ok Nil) (Ok None))
|
||||
((Err e) (Err e))))
|
||||
|
||||
;; The rows as they came, for a projection: `(row-field row "name")`.
|
||||
(: fetch-rows (-bjo-> Db (Table %row %col %test %set) (Select %col %test) (Result SqlError (List Row))))
|
||||
(defbjo (fetch-rows db table q) (query-sql db (select->sql table q)))
|
||||
|
||||
;; An update or a delete, answering how many rows it changed.
|
||||
(: execute (-bjo-> Db (Table %row %col %test %set) (Change %set %test) (Result SqlError int)))
|
||||
(defbjo (execute db table change)
|
||||
(match (change->sql table change)
|
||||
((Ok s) (execute-sql db s))
|
||||
((Err e) (Err e))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Transactions
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(: run (-bjo-> Db string (Result SqlError int)))
|
||||
(defbjo (run db text) (execute-sql db (sql text Nil)))
|
||||
|
||||
;; Runs `f` with `tx` between `begin` and `commit`, and runs `undo` when `f`
|
||||
;; answers `Err` or throws. A failed `undo` is not reported: the error that
|
||||
;; caused it is, and a connection given back to the pool in the middle of a
|
||||
;; transaction is rolled back by Npgsql.
|
||||
(: in-transaction (-bjo-> Db string string (List string) (-bjo-> Db (Result SqlError %a)) (Result SqlError %a)))
|
||||
(defbjo (in-transaction tx begin commit undo f)
|
||||
(defun (rollback) : void
|
||||
(loop (:for text undo) (:do (ignore (run tx text)))))
|
||||
(match (run tx begin)
|
||||
((Err e) (Err e))
|
||||
((Ok _)
|
||||
(match (try (f tx) #:catch (System.Exception))
|
||||
((Ok (Ok x))
|
||||
(match (run tx commit)
|
||||
((Ok _) (Ok x))
|
||||
((Err e) (rollback) (Err e))))
|
||||
((Ok (Err e)) (rollback) (Err e))
|
||||
((Err thrown) (rollback) (raise thrown))))))
|
||||
|
||||
;; Runs `f` in a transaction: committed when it answers `Ok`, rolled back when
|
||||
;; it answers `Err` or throws. `f` gets a `Db` that runs everything on the
|
||||
;; transaction's connection; use that one, not the pool, inside `f`. A
|
||||
;; transaction inside it is a savepoint, so it can fail and be rolled back
|
||||
;; while the outer one goes on.
|
||||
;;
|
||||
;; The `Db` that `f` gets is one connection, so do not use it from two fibers
|
||||
;; at once.
|
||||
(: with-transaction (-bjo-> Db (-bjo-> Db (Result SqlError %a)) (Result SqlError %a)))
|
||||
(defbjo (with-transaction db f)
|
||||
(match (record-ref db connection)
|
||||
(None
|
||||
(match (pg (bjoroutine () (open-connection (record-ref db source))))
|
||||
((Err e) (Err e))
|
||||
((Ok c)
|
||||
(try
|
||||
(in-transaction (record-set db (connection (Some c)) (depth 1))
|
||||
"BEGIN" "COMMIT" (list "ROLLBACK") f)
|
||||
#:finally (connection-dispose c)))))
|
||||
((Some _)
|
||||
(def depth (record-ref db depth))
|
||||
(def savepoint (str "bjopg_" (int->string depth)))
|
||||
(in-transaction (record-set db (depth (+ depth 1)))
|
||||
(str "SAVEPOINT " savepoint)
|
||||
(str "RELEASE " savepoint)
|
||||
(list (str "ROLLBACK TO " savepoint) (str "RELEASE " savepoint))
|
||||
f))))
|
||||
292
tests/pg.bjo
Normal file
292
tests/pg.bjo
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
;; bjopg/tests/pg.bjo — against a real PostgreSQL.
|
||||
;;
|
||||
;; BJOPG_TEST="Host=localhost;Database=bjosql_test" bjo run tests/pg.bjo
|
||||
;;
|
||||
;; The tables it makes are dropped first and last. Without BJOPG_TEST it says
|
||||
;; so and runs nothing. Every other line should start with "ok:".
|
||||
|
||||
(import (std prelude))
|
||||
(import (std simpletest))
|
||||
(import (std datetime))
|
||||
(import (std uuid))
|
||||
(import (bjosql core))
|
||||
(import (bjopg core))
|
||||
|
||||
(import/class
|
||||
(Exception (: System.Exception))
|
||||
(InvalidOperation (: System.InvalidOperationException (-> string InvalidOperation))))
|
||||
|
||||
(def/table events Event #:sql-name "bjopg_events"
|
||||
(key Uuid #:primary-key)
|
||||
(day Date)
|
||||
(at Time)
|
||||
(local DateTime)
|
||||
(happened Instant)
|
||||
(price decimal)
|
||||
(doc JsonDoc)
|
||||
(count int)
|
||||
(note (Option Instant))
|
||||
(tag (Option Uuid)))
|
||||
|
||||
(def/table people Person #:sql-name "bjopg_people"
|
||||
(id int #:primary-key #:auto)
|
||||
(name string)
|
||||
(email (Option string))
|
||||
(score double)
|
||||
(admin bool)
|
||||
(avatar (Option (Array byte))))
|
||||
|
||||
;; A type of one's own, which PostgreSQL has and bjosql does not.
|
||||
(type (: Inet (Struct (: text string))))
|
||||
|
||||
(impl (ToSql Inet) (defun (to-sql a) (SqlOther "inet" (struct-ref a text))))
|
||||
|
||||
(impl (FromSql Inet)
|
||||
(defun (from-sql v)
|
||||
(match v
|
||||
((SqlOther "inet" s) (Ok (Inet (text s))))
|
||||
(_ (Err (str "expected an inet, got " (describe-sql-value v)))))))
|
||||
|
||||
(def/table hosts Host #:sql-name "bjopg_hosts"
|
||||
(name string #:primary-key)
|
||||
(addr Inet #:sql-type "INET"))
|
||||
|
||||
(: some! (-> (Option %a) %a))
|
||||
(defun (some! o)
|
||||
(match o
|
||||
((Some x) x)
|
||||
(None (panic! "expected a value"))))
|
||||
|
||||
(: instant (-> string Instant))
|
||||
(defun (instant s) (some! (iso8601->instant s)))
|
||||
|
||||
(: ok! (-> (Result SqlError %a) %a))
|
||||
(defun (ok! r)
|
||||
(match r
|
||||
((Ok x) x)
|
||||
((Err e) (panic! (sql-error-message e)))))
|
||||
|
||||
(: code-of (-> (Result SqlError %a) string))
|
||||
(defun (code-of r)
|
||||
(match r
|
||||
((Ok _) "ok")
|
||||
((Err (DbError code _)) code)
|
||||
((Err e) (sql-error-message e))))
|
||||
|
||||
(: event (-> Uuid string string Event))
|
||||
(defun (event key when price)
|
||||
(Event (key key)
|
||||
(day (some! (date 2026 10 12)))
|
||||
(at (some! (time 14 5 #:seconds 9 #:nanoseconds 250000000)))
|
||||
(local (some! (datetime 2026 10 12 #:hours 14 #:minutes 5 #:seconds 9 #:nanoseconds 123456000)))
|
||||
(happened (instant when))
|
||||
(price (string->decimal price))
|
||||
(doc (json-doc "{\"b\": 1, \"a\": [1,2]}"))
|
||||
(count 3)
|
||||
(note None)
|
||||
(tag None)))
|
||||
|
||||
(: person (-> string Person))
|
||||
(defun (person name)
|
||||
(Person (id 0) (name name) (email None) (score 1.0) (admin #f) (avatar None)))
|
||||
|
||||
;; A column read as a long.
|
||||
(: long-field (-> Row string (Result SqlError long)))
|
||||
(defun (long-field row name) (row-field row name))
|
||||
|
||||
;; One column of every row, as text, in the order the server gave them.
|
||||
(: column (-bjo-> Db string (List string)))
|
||||
(defbjo (column db query)
|
||||
(list-map (fun (row) (match (row-value row "c")
|
||||
((Some (SqlText s)) s)
|
||||
((Some v) (describe-sql-value v))
|
||||
(None "none")))
|
||||
(ok! (query-sql db (sql query Nil)))))
|
||||
|
||||
(: names (-bjo-> Db (List string)))
|
||||
(defbjo (names db)
|
||||
(list-map (fun (p) (record-ref p name)) (ok! (fetch db people '(select (order-by name))))))
|
||||
|
||||
(: drop-all (-bjo-> Db void))
|
||||
(defbjo (drop-all db)
|
||||
(ignore (ok! (execute-sql db (sql "DROP TABLE IF EXISTS bjopg_events, bjopg_people, bjopg_hosts" Nil)))))
|
||||
|
||||
(: run (-bjo-> Db void))
|
||||
(defbjo (run db)
|
||||
(drop-all db)
|
||||
(ignore (ok! (create-table db events)))
|
||||
(ignore (ok! (create-table db people)))
|
||||
(ignore (ok! (create-table db hosts)))
|
||||
(expect "the columns have PostgreSQL's own types"
|
||||
(column db "SELECT data_type AS c FROM information_schema.columns WHERE table_name = 'bjopg_events' ORDER BY ordinal_position")
|
||||
(list "uuid" "date" "time without time zone" "timestamp without time zone" "timestamp with time zone"
|
||||
"numeric" "jsonb" "integer" "timestamp with time zone" "uuid"))
|
||||
|
||||
;; ---- written and read back ---------------------------------------------
|
||||
(def first (event (uuid-v7) "2026-10-12T09:00:00.5Z" "12.50"))
|
||||
(def stored (ok! (insert db events first)))
|
||||
(expect "a row of every new type comes back equal"
|
||||
(list (= (record-ref stored key) (record-ref first key))
|
||||
(= (compare (record-ref stored day) (record-ref first day)) 0)
|
||||
(= (compare (record-ref stored at) (record-ref first at)) 0)
|
||||
(= (compare (record-ref stored local) (record-ref first local)) 0)
|
||||
(= (compare (record-ref stored happened) (record-ref first happened)) 0)
|
||||
(= (decimal->string (record-ref stored price)) "12.50")
|
||||
(= (record-ref stored count) 3)
|
||||
(none? (record-ref stored note))
|
||||
(none? (record-ref stored tag)))
|
||||
(list #t #t #t #t #t #t #t #t #t))
|
||||
(expect "jsonb keeps the value, in its own form"
|
||||
(json-doc-text (record-ref stored doc))
|
||||
"{\"a\": [1, 2], \"b\": 1}")
|
||||
(expect "the instant is stored as a point in time"
|
||||
(column db "SELECT to_char(happened AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.US') AS c FROM bjopg_events")
|
||||
(list "2026-10-12 09:00:00.500000"))
|
||||
|
||||
;; ---- queries ------------------------------------------------------------
|
||||
(ignore (ok! (insert db events (event (uuid-v7) "2026-10-12T09:00:00.25Z" "7.00"))))
|
||||
(ignore (ok! (insert db events (event (uuid-v7) "2026-10-12T09:00:00Z" "100.00"))))
|
||||
(ignore (ok! (insert db events (event (uuid-v7) "2026-10-12T10:00:00+02:00" "-3.00"))))
|
||||
(expect "ORDER BY an instant"
|
||||
(list-map (fun (e) (->str (record-ref e happened))) (ok! (fetch db events '(select (order-by happened)))))
|
||||
(list "2026-10-12T08:00:00Z" "2026-10-12T09:00:00Z" "2026-10-12T09:00:00.25Z" "2026-10-12T09:00:00.5Z"))
|
||||
(expect "ORDER BY a version 7 uuid sorts by the time it was made"
|
||||
(list-map (fun (e) (decimal->string (record-ref e price))) (ok! (fetch db events '(select (order-by key)))))
|
||||
(list "12.50" "7.00" "100.00" "-3.00"))
|
||||
(expect "ORDER BY a decimal is numeric, as SQLite's text is not"
|
||||
(list-map (fun (e) (decimal->string (record-ref e price))) (ok! (fetch db events '(select (order-by price)))))
|
||||
(list "-3.00" "7.00" "12.50" "100.00"))
|
||||
(expect "a test on a decimal is numeric"
|
||||
(list-length (ok! (fetch db events '(select (where (price (> 10.0M)))))))
|
||||
2)
|
||||
(expect "a test on an instant"
|
||||
(list-length (ok! (fetch db events '(select (where (happened (>= ,(instant "2026-10-12T09:00:00.25Z"))))))))
|
||||
2)
|
||||
(expect "a test on a uuid"
|
||||
(list-length (ok! (fetch db events '(select (where (key (= ,(record-ref first key))))))))
|
||||
1)
|
||||
(def label (uuid-v4))
|
||||
(expect "a nullable uuid and instant set from values"
|
||||
(ok! (execute db events '(update (set (tag ,(Some label)) (note ,(Some (instant "2026-01-01T00:00:00Z"))))
|
||||
(where (key (= ,(record-ref first key)))))))
|
||||
1)
|
||||
(expect "and read back"
|
||||
(match (ok! (fetch-one db events '(select (where (tag not-null)))))
|
||||
((Some e) (list (if (= (some! (record-ref e tag)) label) "the label" "another uuid")
|
||||
(->str (record-ref e note))))
|
||||
(None (list "none")))
|
||||
(list "the label" "(Some 2026-01-01T00:00:00Z)"))
|
||||
(expect "set back to NULL"
|
||||
(ok! (execute db events '(update (set (tag null) (note null)) (where (tag not-null)))))
|
||||
1)
|
||||
|
||||
;; ---- the six that were there --------------------------------------------
|
||||
(def ada (ok! (insert db people (Person (id 0) (name "Ada") (email (Some "ada@example.org")) (score 9.5)
|
||||
(admin #t) (avatar (Some #[1uy 2uy 3uy]))))))
|
||||
(expect "an #:auto key comes back" (record-ref ada id) 1)
|
||||
(expect "the rest of the row"
|
||||
(str (record-ref ada name) " " (->str (record-ref ada email)) " " (double->string (record-ref ada score))
|
||||
" " (->str (record-ref ada admin)) " "
|
||||
(match (record-ref ada avatar) ((Some b) (int->string (array-length b))) (None "none")))
|
||||
"Ada (Some ada@example.org) 9.5 True 3")
|
||||
(expect "insert-all"
|
||||
(list-map (fun (p) (record-ref p id)) (ok! (insert-all db people (list (person "Grace") (person "Linus")))))
|
||||
(list 2 3))
|
||||
(expect "SQL by hand, with ? for a value"
|
||||
(match (ok! (query-sql db (sql "SELECT count(*) AS n FROM bjopg_people WHERE score > ?" (list (to-sql 5.0)))))
|
||||
((Cons row _) (match (long-field row "n") ((Ok n) n) ((Err _) -1L)))
|
||||
(Nil -1L))
|
||||
1L)
|
||||
(expect "an update counts its rows" (ok! (execute db people '(update (set (score 2.0)) (where (admin (= #f)))))) 2)
|
||||
(expect "a delete counts its rows" (ok! (execute db people '(delete (where (name (= "Linus")))))) 1)
|
||||
|
||||
;; ---- errors ---------------------------------------------------------------
|
||||
(ignore (ok! (execute-sql db (sql "CREATE UNIQUE INDEX bjopg_people_name ON bjopg_people (name)" Nil))))
|
||||
(expect "a unique violation is SQLSTATE 23505" (code-of (insert db people (person "Ada"))) "23505")
|
||||
(expect "SQL the server cannot parse is 42601" (code-of (execute-sql db (sql "SELEKT 1" Nil))) "42601")
|
||||
|
||||
;; ---- transactions -----------------------------------------------------------
|
||||
(expect "Ok commits"
|
||||
(code-of (with-transaction db (bjoroutine (tx) (insert tx people (person "Barbara")))))
|
||||
"ok")
|
||||
(expect "Err rolls back"
|
||||
(code-of (with-transaction db
|
||||
(bjoroutine (tx)
|
||||
(ignore (ok! (insert tx people (person "Edsger"))))
|
||||
(Err (BadQuery "changed my mind")))))
|
||||
"changed my mind")
|
||||
(expect "a throw rolls back, and is thrown on"
|
||||
(match (try (with-transaction db
|
||||
(bjoroutine (tx)
|
||||
(ignore (ok! (insert tx people (person "Niklaus"))))
|
||||
(raise (cast Exception (InvalidOperation. "boom")))))
|
||||
#:catch (System.Exception))
|
||||
((Ok _) "returned")
|
||||
((Err _) "thrown"))
|
||||
"thrown")
|
||||
(expect "after a failed savepoint, the outer transaction goes on"
|
||||
(code-of (with-transaction db
|
||||
(bjoroutine (tx)
|
||||
(ignore (ok! (insert tx people (person "Alan"))))
|
||||
(expect "the inner one fails on the unique name"
|
||||
(code-of (with-transaction tx (bjoroutine (inner) (insert inner people (person "Ada")))))
|
||||
"23505")
|
||||
(insert tx people (person "Tony")))))
|
||||
"ok")
|
||||
(expect "who is there"
|
||||
(names db)
|
||||
(list "Ada" "Alan" "Barbara" "Grace" "Tony"))
|
||||
|
||||
;; ---- a type of one's own ------------------------------------------------------
|
||||
(ignore (ok! (insert db hosts (Host (name "router") (addr (Inet (text "10.0.0.1")))))))
|
||||
(expect "SqlOther is sent with a cast to its type"
|
||||
(list-map (fun (h) (struct-ref (record-ref h addr) text))
|
||||
(ok! (fetch db hosts '(select (where (addr (= ,(Inet (text "10.0.0.1")))))))))
|
||||
(list "10.0.0.1"))
|
||||
(expect "and the server checks it"
|
||||
(code-of (insert db hosts (Host (name "bad") (addr (Inet (text "not an address"))))))
|
||||
"22P02")
|
||||
|
||||
;; ---- many fibers, one pool ------------------------------------------------------
|
||||
;; Each query waits a little on the server, so the 20 overlap: they are only
|
||||
;; done in a fraction of 20 times the wait if the fibers suspend while they
|
||||
;; wait, rather than holding a thread each.
|
||||
(def started (monotonic-ms))
|
||||
(def queries
|
||||
(loop (:for i (range 0 20))
|
||||
(:acc out (listing (bjo (query-sql db (sql "SELECT count(*) AS n, pg_sleep(0.2) FROM bjopg_people" Nil)))))
|
||||
=> out))
|
||||
(def answers
|
||||
(loop (:for p queries)
|
||||
(:acc out (listing (match (sync (promise-join p))
|
||||
((Ok (Ok (Cons row _))) (match (long-field row "n") ((Ok n) n) ((Err e) -1L)))
|
||||
((Ok (Err e)) -1L)
|
||||
((Ok (Ok Nil)) -2L)
|
||||
((Err e) -3L))))
|
||||
=> out))
|
||||
(expect "20 queries at once through the pool all answer" answers (list-map (fun (_) 5L) (range-list 0 20)))
|
||||
(expect-true "and they overlap" (< (- (monotonic-ms) started) 2000L))
|
||||
|
||||
(drop-all db))
|
||||
|
||||
(: range-list (-> int int (List int)))
|
||||
(defun (range-list a b) (loop (:for i (range a b)) (:acc out (listing i)) => out))
|
||||
|
||||
(defbjo (main args)
|
||||
(match (get-environment-variable "BJOPG_TEST")
|
||||
(None (println "skipped: set BJOPG_TEST to a connection string, such as Host=localhost;Database=bjosql_test"))
|
||||
((Some connection-string)
|
||||
(expect "a server that is not there is DbError 08000"
|
||||
(code-of (open-db "Host=localhost;Port=1;Database=nothing;Timeout=2"))
|
||||
"08000")
|
||||
(expect "a database that is not there is the server's 3D000"
|
||||
(code-of (open-db (str connection-string ";Database=bjopg_no_such_database")))
|
||||
"3D000")
|
||||
(match (open-db connection-string)
|
||||
((Err e) (println (str "FAILURE: open: " (sql-error-message e))))
|
||||
((Ok db) (try (run db) #:finally (close-db db))))))
|
||||
0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue