From efc70c32362eda2840c3c2a5fdf7d1eb876afd7f Mon Sep 17 00:00:00 2001 From: Karthik Ganeshram Date: Fri, 5 Sep 2025 09:05:56 +0200 Subject: [PATCH 1/2] add sqlitev3 package Signed-off-by: Karthik Ganeshram --- packages/spin-sqlitev3/LICENSE | 217 ++++++++++++++++++ packages/spin-sqlitev3/README.md | 3 + packages/spin-sqlitev3/package-lock.json | 30 +++ packages/spin-sqlitev3/package.json | 30 +++ packages/spin-sqlitev3/src/index.ts | 183 +++++++++++++++ packages/spin-sqlitev3/tsconfig.json | 19 ++ .../types/spin-sqlite-sqlite.d.ts | 109 +++++++++ packages/spin-sqlitev3/wit/world.wit | 64 ++++++ 8 files changed, 655 insertions(+) create mode 100644 packages/spin-sqlitev3/LICENSE create mode 100644 packages/spin-sqlitev3/README.md create mode 100644 packages/spin-sqlitev3/package-lock.json create mode 100644 packages/spin-sqlitev3/package.json create mode 100644 packages/spin-sqlitev3/src/index.ts create mode 100644 packages/spin-sqlitev3/tsconfig.json create mode 100644 packages/spin-sqlitev3/types/spin-sqlite-sqlite.d.ts create mode 100644 packages/spin-sqlitev3/wit/world.wit diff --git a/packages/spin-sqlitev3/LICENSE b/packages/spin-sqlitev3/LICENSE new file mode 100644 index 00000000..00247da1 --- /dev/null +++ b/packages/spin-sqlitev3/LICENSE @@ -0,0 +1,217 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) The Spin Framework Contributors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. \ No newline at end of file diff --git a/packages/spin-sqlitev3/README.md b/packages/spin-sqlitev3/README.md new file mode 100644 index 00000000..00448d21 --- /dev/null +++ b/packages/spin-sqlitev3/README.md @@ -0,0 +1,3 @@ +# Spin SQLite + +This package provides bindings that enable using the Spin SQLite v3 interface in apps built using the `@spinframework/build-tools`. \ No newline at end of file diff --git a/packages/spin-sqlitev3/package-lock.json b/packages/spin-sqlitev3/package-lock.json new file mode 100644 index 00000000..7bd1ca29 --- /dev/null +++ b/packages/spin-sqlitev3/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@spinframework/spin-sqlite", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@spinframework/spin-sqlite", + "version": "1.0.0", + "license": " Apache-2.0 WITH LLVM-exception", + "devDependencies": { + "typescript": "^5.9.2" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/packages/spin-sqlitev3/package.json b/packages/spin-sqlitev3/package.json new file mode 100644 index 00000000..dda8ed1a --- /dev/null +++ b/packages/spin-sqlitev3/package.json @@ -0,0 +1,30 @@ +{ + "name": "@spinframework/spin-sqlitev3", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc && cp -r types/* dist/" + }, + "keywords": [], + "author": "", + "license": " Apache-2.0 WITH LLVM-exception", + "devDependencies": { + "typescript": "^5.9.2" + }, + "files": [ + "dist", + "wit" + ], + "config": { + "witDependencies": [ + { + "witPath": "./wit", + "package": "fermyon:spin@3.0.0", + "world": "spin-sqlitev3" + } + ] + } +} \ No newline at end of file diff --git a/packages/spin-sqlitev3/src/index.ts b/packages/spin-sqlitev3/src/index.ts new file mode 100644 index 00000000..fd07e0eb --- /dev/null +++ b/packages/spin-sqlitev3/src/index.ts @@ -0,0 +1,183 @@ +import * as spinSqlite from 'spin:sqlite/sqlite@3.0.0'; + +export type sqliteValues = + | ValueInteger + | ValueReal + | ValueText + | ValueBlob + | ValueNull; +export type ParameterValue = + | sqliteValues + | number + | bigint + | null + | string + | Uint8Array; +type SqliteRowResultItem = { + tag: string; + val: number | bigint | string | Uint8Array | null; +}; +type SqliteRowResult = { values: SqliteRowResultItem[] }; +export type ValueInteger = { tag: 'integer'; val: bigint }; +export type ValueReal = { tag: 'real'; val: number }; +export type ValueText = { tag: 'text'; val: string }; +export type ValueBlob = { tag: 'blob'; val: Uint8Array }; +export type ValueNull = { tag: 'null' }; + +/** + * Interface representing the result of an SQLite query. + * @interface SpinSqliteResult + * @property {string[]} columns - The column names in the result. + * @property {SqliteRowResult[]} rows - The rows of results. + */ +interface SpinSqliteResult { + columns: string[]; + rows: SqliteRowResult[]; +} + +/** + * Interface representing the formatted result of an SQLite query. + * @interface SqliteResult + * @property {string[]} columns - The column names in the result. + * @property {Object[]} rows - The rows of results. + */ +export interface SqliteResult { + columns: string[]; + rows: { [key: string]: number | bigint | null | string | Uint8Array }[]; +} + +/** + * Interface representing a connection to an SQLite database with a method for executing queries. + * @interface SqliteConnection + */ +export interface SqliteConnection { + /** + * Executes an SQLite statement with given parameters and returns the result. + * @param {string} statement - The SQL statement to execute. + * @param {ParameterValue[]} parameters - The parameters for the SQL statement. + * @returns {SqliteResult} + */ + execute: (statement: string, parameters: ParameterValue[]) => SqliteResult; + + /** + * Returns the row ID of the last inserted row of the most recent successful + * INSERT operation on the connection or 0 if no such operation has occurred. + * @returns {bigint} + */ + lastInsertRowId: () => bigint; + + /** + * The number of rows modified, inserted or deleted by the most recently completed + * INSERT, UPDATE or DELETE statement on the connection. + * @returns {bigint} T + */ + changes: () => bigint; +} + +function createSqliteConnection( + connection: spinSqlite.Connection, +): SqliteConnection { + return { + execute: ( + statement: string, + parameters: ParameterValue[], + ): SqliteResult => { + let santizedParams = convertToWitTypes(parameters); + let ret = connection.execute( + statement, + santizedParams, + ) as SpinSqliteResult; + let results: SqliteResult = { + columns: ret.columns, + rows: [], + }; + ret.rows.map((k: SqliteRowResult, rowIndex: number) => { + results.rows.push({}); + k.values.map((val, valIndex: number) => { + results.rows[rowIndex][results.columns[valIndex]] = val.val; + }); + }); + return results; + }, + lastInsertRowId: (): bigint => { + return connection.lastInsertRowid(); + }, + changes: (): bigint => { + return connection.changes(); + }, + }; +} + +/** + * Opens a connection to the SQLite database with the specified label. + * @param {string} label - The label of the database to connect to. + * @returns {SqliteConnection} The SQLite connection object. + */ +export function open(label: string): SqliteConnection { + return createSqliteConnection(spinSqlite.Connection.open(label)); +} + +/** + * Opens a connection to the default SQLite database. + * @returns {SqliteConnection} The SQLite connection object. + */ +export function openDefault(): SqliteConnection { + return createSqliteConnection(spinSqlite.Connection.open('default')); +} + +const valueInteger = (value: bigint): ValueInteger => { + return { tag: 'integer', val: value }; +}; + +const valueReal = (value: number): ValueReal => { + return { tag: 'real', val: value }; +}; + +const valueText = (value: string): ValueText => { + return { tag: 'text', val: value }; +}; + +const valueBlob = (value: Uint8Array): ValueBlob => { + return { tag: 'blob', val: value }; +}; + +const valueNull = (): ValueNull => { + return { tag: 'null' }; +}; + +function convertToWitTypes(parameters: ParameterValue[]): sqliteValues[] { + let sanitized: sqliteValues[] = []; + for (let k of parameters) { + if (typeof k === 'object') { + sanitized.push(k as sqliteValues); + continue; + } + if (typeof k === 'number') { + isFloat(k) + ? sanitized.push(valueReal(k)) + : sanitized.push(valueInteger(BigInt(k))); + continue; + } + if (typeof k === 'bigint') { + sanitized.push(valueInteger(k)); + continue; + } + if (typeof k === 'string') { + sanitized.push(valueText(k)); + continue; + } + if (k === null) { + sanitized.push(valueNull()); + continue; + } + if ((k as any) instanceof Uint8Array) { + sanitized.push(valueBlob(k)); + continue; + } + } + return sanitized; +} + +function isFloat(number: number) { + return number % 1 !== 0; +} diff --git a/packages/spin-sqlitev3/tsconfig.json b/packages/spin-sqlitev3/tsconfig.json new file mode 100644 index 00000000..acfaf82e --- /dev/null +++ b/packages/spin-sqlitev3/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "nodenext", + "lib": [ + "ESNext", + "WebWorker" + ], + "moduleResolution": "nodenext", + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/spin-sqlitev3/types/spin-sqlite-sqlite.d.ts b/packages/spin-sqlitev3/types/spin-sqlite-sqlite.d.ts new file mode 100644 index 00000000..a99042fd --- /dev/null +++ b/packages/spin-sqlitev3/types/spin-sqlite-sqlite.d.ts @@ -0,0 +1,109 @@ +declare module 'spin:sqlite/sqlite@3.0.0' { + /** + * The set of errors which may be raised by functions in this interface + */ + export type Error = ErrorNoSuchDatabase | ErrorAccessDenied | ErrorInvalidConnection | ErrorDatabaseFull | ErrorIo; + /** + * The host does not recognize the database name requested. + */ + export interface ErrorNoSuchDatabase { + tag: 'no-such-database', + } + /** + * The requesting component does not have access to the specified database (which may or may not exist). + */ + export interface ErrorAccessDenied { + tag: 'access-denied', + } + /** + * The provided connection is not valid + */ + export interface ErrorInvalidConnection { + tag: 'invalid-connection', + } + /** + * The database has reached its capacity + */ + export interface ErrorDatabaseFull { + tag: 'database-full', + } + /** + * Some implementation-specific error has occurred (e.g. I/O) + */ + export interface ErrorIo { + tag: 'io', + val: string, + } + /** + * A single column's result from a database query + */ + export type Value = ValueInteger | ValueReal | ValueText | ValueBlob | ValueNull; + export interface ValueInteger { + tag: 'integer', + val: bigint, + } + export interface ValueReal { + tag: 'real', + val: number, + } + export interface ValueText { + tag: 'text', + val: string, + } + export interface ValueBlob { + tag: 'blob', + val: Uint8Array, + } + export interface ValueNull { + tag: 'null', + } + /** + * A set of values for each of the columns in a query-result + */ + export interface RowResult { + values: Array, + } + /** + * A result of a query + */ + export interface QueryResult { + /** + * The names of the columns retrieved in the query + */ + columns: Array, + /** + * the row results each containing the values for all the columns for a given row + */ + rows: Array, + } + + export class Connection implements Disposable { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Open a connection to a named database instance. + * + * If `database` is "default", the default instance is opened. + * + * `error::no-such-database` will be raised if the `name` is not recognized. + */ + static open(database: string): Connection; + /** + * Execute a statement returning back data if there is any + */ + execute(statement: string, parameters: Array): QueryResult; + /** + * The SQLite rowid of the most recent successful INSERT on the connection, or 0 if + * there has not yet been an INSERT on the connection. + */ + lastInsertRowid(): bigint; + /** + * The number of rows modified, inserted or deleted by the most recently completed + * INSERT, UPDATE or DELETE statement on the connection. + */ + changes(): bigint; + [Symbol.dispose](): void; + } +} diff --git a/packages/spin-sqlitev3/wit/world.wit b/packages/spin-sqlitev3/wit/world.wit new file mode 100644 index 00000000..b6288c35 --- /dev/null +++ b/packages/spin-sqlitev3/wit/world.wit @@ -0,0 +1,64 @@ +package spin:sqlite@3.0.0; + +interface sqlite { + /// A handle to an open sqlite instance + resource connection { + /// Open a connection to a named database instance. + /// + /// If `database` is "default", the default instance is opened. + /// + /// `error::no-such-database` will be raised if the `name` is not recognized. + open: static func(database: string) -> result; + + /// Execute a statement returning back data if there is any + execute: func(statement: string, parameters: list) -> result; + + /// The SQLite rowid of the most recent successful INSERT on the connection, or 0 if + /// there has not yet been an INSERT on the connection. + last-insert-rowid: func() -> s64; + + /// The number of rows modified, inserted or deleted by the most recently completed + /// INSERT, UPDATE or DELETE statement on the connection. + changes: func() -> u64; + } + + /// The set of errors which may be raised by functions in this interface + variant error { + /// The host does not recognize the database name requested. + no-such-database, + /// The requesting component does not have access to the specified database (which may or may not exist). + access-denied, + /// The provided connection is not valid + invalid-connection, + /// The database has reached its capacity + database-full, + /// Some implementation-specific error has occurred (e.g. I/O) + io(string) + } + + /// A result of a query + record query-result { + /// The names of the columns retrieved in the query + columns: list, + /// the row results each containing the values for all the columns for a given row + rows: list, + } + + /// A set of values for each of the columns in a query-result + record row-result { + values: list + } + + /// A single column's result from a database query + variant value { + integer(s64), + real(f64), + text(string), + blob(list), + null + } +} + +world spin-sqlitev3 { + import sqlite; +} \ No newline at end of file From 9152d67bf420a18a9db7b630fe0bb2851f957d47 Mon Sep 17 00:00:00 2001 From: Karthik Ganeshram Date: Fri, 5 Sep 2025 09:41:15 +0200 Subject: [PATCH 2/2] add spin-postgresv4 package Signed-off-by: Karthik Ganeshram --- packages/spin-postgresv4/LICENSE | 217 +++++++++ packages/spin-postgresv4/README.md | 3 + packages/spin-postgresv4/package-lock.json | 30 ++ packages/spin-postgresv4/package.json | 30 ++ packages/spin-postgresv4/src/index.ts | 391 ++++++++++++++++ packages/spin-postgresv4/tsconfig.json | 19 + .../types/spin-postgres-postgres.d.ts | 425 ++++++++++++++++++ packages/spin-postgresv4/wit/world.wit | 167 +++++++ 8 files changed, 1282 insertions(+) create mode 100644 packages/spin-postgresv4/LICENSE create mode 100644 packages/spin-postgresv4/README.md create mode 100644 packages/spin-postgresv4/package-lock.json create mode 100644 packages/spin-postgresv4/package.json create mode 100644 packages/spin-postgresv4/src/index.ts create mode 100644 packages/spin-postgresv4/tsconfig.json create mode 100644 packages/spin-postgresv4/types/spin-postgres-postgres.d.ts create mode 100644 packages/spin-postgresv4/wit/world.wit diff --git a/packages/spin-postgresv4/LICENSE b/packages/spin-postgresv4/LICENSE new file mode 100644 index 00000000..00247da1 --- /dev/null +++ b/packages/spin-postgresv4/LICENSE @@ -0,0 +1,217 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) The Spin Framework Contributors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. \ No newline at end of file diff --git a/packages/spin-postgresv4/README.md b/packages/spin-postgresv4/README.md new file mode 100644 index 00000000..58a4a88c --- /dev/null +++ b/packages/spin-postgresv4/README.md @@ -0,0 +1,3 @@ +# Spin Postgres v3 + +This package provides bindings that enable using the Spin Postgres v4 interface in apps built using the `@spinframework/build-tools`. \ No newline at end of file diff --git a/packages/spin-postgresv4/package-lock.json b/packages/spin-postgresv4/package-lock.json new file mode 100644 index 00000000..82f8acd5 --- /dev/null +++ b/packages/spin-postgresv4/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@spinframework/spin-postgres3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@spinframework/spin-postgres3", + "version": "1.0.0", + "license": " Apache-2.0 WITH LLVM-exception", + "devDependencies": { + "typescript": "^5.7.3" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/packages/spin-postgresv4/package.json b/packages/spin-postgresv4/package.json new file mode 100644 index 00000000..fd639a3e --- /dev/null +++ b/packages/spin-postgresv4/package.json @@ -0,0 +1,30 @@ +{ + "name": "@spinframework/spin-postgres3", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc && cp -r types/* dist/" + }, + "keywords": [], + "author": "", + "license": " Apache-2.0 WITH LLVM-exception", + "devDependencies": { + "typescript": "^5.7.3" + }, + "files": [ + "dist", + "wit" + ], + "config": { + "witDependencies": [ + { + "witPath": "./wit", + "package": "spin:postgres@4.0.0", + "world": "spin-postgres4" + } + ] + } +} \ No newline at end of file diff --git a/packages/spin-postgresv4/src/index.ts b/packages/spin-postgresv4/src/index.ts new file mode 100644 index 00000000..d2b3fc26 --- /dev/null +++ b/packages/spin-postgresv4/src/index.ts @@ -0,0 +1,391 @@ +import * as spinPg from 'spin:postgres/postgres@4.0.0'; + +export type RangeBoundKind = 'inclusive' | 'exclusive'; + +export type SpinPostgresV4ValueBoolean = { tag: 'boolean'; val: boolean }; +export type SpinPostgresV4ValueInt8 = { tag: 'int8'; val: number }; +export type SpinPostgresV4ValueInt16 = { tag: 'int16'; val: number }; +export type SpinPostgresV4ValueInt32 = { tag: 'int32'; val: number }; +export type SpinPostgresV4ValueInt64 = { tag: 'int64'; val: bigint }; +export type SpinPostgresV4ValueFloating32 = { tag: 'floating32'; val: number }; +export type SpinPostgresV4ValueFloating64 = { tag: 'floating64'; val: number }; +export type SpinPostgresV4ValueStr = { tag: 'str'; val: string }; +export type SpinPostgresV4ValueBinary = { tag: 'binary'; val: Uint8Array }; +export type SpinPostgresV4ValueDate = { tag: 'date'; val: [number, number, number]; }; +export type SpinPostgresV4ValueTime = { tag: 'time'; val: [number, number, number, number]; }; +export type SpinPostgresV4ValueDateTime = { + tag: 'datetime'; + val: [number, number, number, number, number, number, number]; +}; +export type SpinPostgresV4ValueTimeStamp = { tag: 'timestamp'; val: bigint }; +export interface SpinPostgresV4DbValueUuid { + tag: 'uuid', + val: string, +} +export interface SpinPostgresV4DbValueJsonb { + tag: 'jsonb', + val: Uint8Array, +} +export interface SpinPostgresV4DbValueDecimal { + tag: 'decimal', + val: string, +} +export interface SpinPostgresV4DbValueRangeInt32 { + tag: 'range-int32', + val: [[number, RangeBoundKind] | undefined, [number, RangeBoundKind] | undefined], +} +export interface SpinPostgresV4DbValueRangeInt64 { + tag: 'range-int64', + val: [[bigint, RangeBoundKind] | undefined, [bigint, RangeBoundKind] | undefined], +} +export interface SpinPostgresV4DbValueRangeDecimal { + tag: 'range-decimal', + val: [[string, RangeBoundKind] | undefined, [string, RangeBoundKind] | undefined], +} +export interface SpinPostgresV4DbValueArrayInt32 { + tag: 'array-int32', + val: Array, +} +export interface SpinPostgresV4DbValueArrayInt64 { + tag: 'array-int64', + val: Array, +} +export interface SpinPostgresV4DbValueArrayDecimal { + tag: 'array-decimal', + val: Array, +} +export interface SpinPostgresV4DbValueArrayStr { + tag: 'array-str', + val: Array, +} +export interface SpinPostgresV4DbValueInterval { + tag: 'interval', + val: spinPg.Interval, +} + +export interface SpinPostgresV4Unsupported { + tag: 'unsupported', + val: Uint8Array, +} + +export type SpinPostgresV4ValueDbNull = { tag: 'db-null' }; + + +export type SpinPostgresV4ParameterValue = + | SpinPostgresV4ValueBoolean + | SpinPostgresV4ValueInt8 + | SpinPostgresV4ValueInt16 + | SpinPostgresV4ValueInt32 + | SpinPostgresV4ValueInt64 + | SpinPostgresV4ValueFloating32 + | SpinPostgresV4ValueFloating64 + | SpinPostgresV4ValueStr + | SpinPostgresV4ValueBinary + | SpinPostgresV4ValueDate + | SpinPostgresV4ValueTime + | SpinPostgresV4ValueDateTime + | SpinPostgresV4ValueTimeStamp + | SpinPostgresV4DbValueUuid + | SpinPostgresV4DbValueJsonb + | SpinPostgresV4DbValueDecimal + | SpinPostgresV4DbValueRangeInt32 + | SpinPostgresV4DbValueRangeInt64 + | SpinPostgresV4DbValueRangeDecimal + | SpinPostgresV4DbValueArrayInt32 + | SpinPostgresV4DbValueArrayInt64 + | SpinPostgresV4DbValueArrayDecimal + | SpinPostgresV4DbValueArrayStr + | SpinPostgresV4DbValueInterval + | SpinPostgresV4ValueDbNull; + +export type PostgresV4ParameterValue = + | SpinPostgresV4ParameterValue + | number + | bigint + | boolean + | null + | Uint8Array + | string; + +export enum PostgresV4DataType { + PostgresV4Boolean = 'boolean', + PostgresV4Int8 = 'int8', + PostgresV4Int16 = 'int16', + PostgresV4Int32 = 'int32', + PostgresV4Int64 = 'int64', + PostgresV4Floating32 = 'floating32', + PostgresV4Floating64 = 'floating64', + PostgresV4Str = 'str', + PostgresV4Binary = 'binary', + PostgresV4Date = 'date', + PostgresV4Time = 'time', + PostgresV4DateTime = 'datetime', + PostgresV4TimeStamp = 'timestamp', + PostgresV4Uuid = 'uuid', + PostgresV4Jsonb = 'jsonb', + PostgresV4Decimal = 'decimal', + PostgresV4RangeInt32 = 'range-int32', + PostgresV4RangeInt64 = 'range-int64', + PostgresV4RangeDecimal = 'range-decimal', + PostgresV4ArrayInt32 = 'array-int32', + PostgresV4ArrayInt64 = 'array-int64', + PostgresV4ArrayDecimal = 'array-decimal', + PostgresV4ArrayStr = 'array-str', + PostgresV4Interval = 'interval', + PostgresV4Other = 'other', +} + +export interface PostgresV4Column { + name: string; + dataType: PostgresV4DataType; +} + +export interface PostgresV4RowSet { + columns: PostgresV4Column[]; + rows: { + [key: string]: + | number + | boolean + | bigint + | null + | string + | Uint8Array + | Date; + }[]; +} + +export type PostgresV4DbBoolean = { tag: 'boolean'; val: boolean }; +export type PostgresV4DbInt8 = { tag: 'int8'; val: number }; +export type PostgresV4DbInt16 = { tag: 'int16'; val: number }; +export type PostgresV4DbInt32 = { tag: 'int32'; val: number }; +export type PostgresV4DbInt64 = { tag: 'int64'; val: number }; +export type PostgresV4DbFloating32 = { tag: 'floating32'; val: number }; +export type PostgresV4DbFloating64 = { tag: 'floating64'; val: number }; +export type PostgresV4DbStr = { tag: 'str'; val: string }; +export type PostgresV4DbBinary = { tag: 'binary'; val: Uint8Array }; +export type PostgresV4DbDate = { tag: 'date'; val: [number, number, number] }; +export type PostgresV4DbTime = { + tag: 'time'; + val: [number, number, number, number]; +}; +export type PostgresV4DbDateTime = { + tag: 'datetime'; + val: [number, number, number, number, number, number, number]; +}; +export type PostgresV4DbTimeastamp = { tag: 'timestamp'; val: bigint }; +export type PostgresV4DbNull = { tag: 'db-null' }; +export type PostgresV4DbUnsupported = { tag: 'unsupported' }; + +export type RdbmsDbValue = + | PostgresV4DbBoolean + | PostgresV4DbInt8 + | PostgresV4DbInt16 + | PostgresV4DbInt32 + | PostgresV4DbInt64 + | PostgresV4DbFloating32 + | PostgresV4DbFloating64 + | PostgresV4DbStr + | PostgresV4DbBinary + | PostgresV4DbDate + | PostgresV4DbTime + | PostgresV4DbDateTime + | PostgresV4DbTimeastamp + | PostgresV4DbNull + | PostgresV4DbUnsupported; + +export type RdbmsRow = RdbmsDbValue[]; + +export interface SpinRdbmsRowSet { + columns: PostgresV4Column[]; + rows: RdbmsRow[]; +} + +/** + * Interface representing a PostgreSQL connection with methods for querying and executing statements. + * @interface PostgresConnection + */ +export interface PostgresConnection { + /** + * Queries the database with the specified statement and parameters. + * @param {string} statement - The SQL statement to execute. + * @param {PostgresV4Value[]} params - The parameters for the SQL statement. + * @returns {RdbmsRowSet} The result set of the query. + */ + query: ( + statement: string, + params: PostgresV4ParameterValue[], + ) => PostgresV4RowSet; + /** + * Executes a statement on the database with the specified parameters. + * @param {string} statement - The SQL statement to execute. + * @param {PostgresV4Value[]} params - The parameters for the SQL statement. + * @returns {number} The number of rows affected by the execution. + */ + execute: (statement: string, params: PostgresV4ParameterValue[]) => bigint; +} + +function createPostgresConnection( + connection: spinPg.Connection, +): PostgresConnection { + return { + query: (statement: string, params: PostgresV4ParameterValue[]) => { + let santizedParams = convertRdbmsToWitTypes(params); + let ret = mapRowSet(connection.query(statement, santizedParams)) as SpinRdbmsRowSet; + let results: PostgresV4RowSet = { + columns: ret.columns, + rows: [], + }; + ret.rows.map((k: RdbmsRow, rowIndex: number) => { + results.rows.push({}); + k.map((val, valIndex: number) => { + switch (val.tag) { + case 'date': { + // Date (year, month, day) + const [year, month, day] = val.val; + results.rows[rowIndex][results.columns[valIndex].name] = new Date( + Date.UTC(year, month - 1, day), + ); // UTC Date object + break; + } + + case 'time': { + // Time (hour, minute, second, nanosecond) + const [hour, minute, second, nanosecond] = val.val; + const date = new Date(Date.UTC(1970, 0, 1, hour, minute, second)); // Using an arbitrary date + date.setMilliseconds(nanosecond / 1_000_000); // Convert nanoseconds to milliseconds + results.rows[rowIndex][results.columns[valIndex].name] = date; // UTC Date object with only time set + break; + } + + case 'datetime': { + // DateTime (year, month, day, hour, minute, second, nanosecond) + const [year, month, day, hour, minute, second, nanosecond] = + val.val; + const date = new Date( + Date.UTC(year, month - 1, day, hour, minute, second), + ); + date.setMilliseconds(nanosecond / 1_000_000); // Convert nanoseconds to milliseconds + results.rows[rowIndex][results.columns[valIndex].name] = date; // Complete UTC Date object + break; + } + + case 'timestamp': { + // Timestamp (seconds since epoch) + const seconds = val.val as unknown as number; + results.rows[rowIndex][results.columns[valIndex].name] = new Date( + seconds * 1000, + ); // Convert seconds to milliseconds + break; + } + + default: { + results.rows[rowIndex][results.columns[valIndex].name] = + val.tag == 'db-null' || val.tag == 'unsupported' + ? null + : val.val; + break; + } + } + }); + }); + return results; + }, + execute: (statement: string, params: PostgresV4ParameterValue[]) => { + let santizedParams = convertRdbmsToWitTypes(params); + let ret = connection.execute(statement, santizedParams) as bigint; + return ret; + }, + }; +} + +/** + * Opens a PostgreSQL connection to the specified address. + * @param {string} address - The address of the PostgreSQL server. + * @returns {PostgresConnection} The PostgreSQL connection object. + */ +export function open(address: string): PostgresConnection { + return createPostgresConnection(spinPg.Connection.open(address)); +} + +function convertRdbmsToWitTypes( + parameters: PostgresV4ParameterValue[], +): SpinPostgresV4ParameterValue[] { + let sanitized: SpinPostgresV4ParameterValue[] = []; + for (let k of parameters) { + if (typeof k === 'object') { + sanitized.push(k as SpinPostgresV4ParameterValue); + continue; + } + if (typeof k === 'string') { + sanitized.push({ tag: 'str', val: k }); + continue; + } + if (typeof k === null) { + sanitized.push({ tag: 'db-null' }); + continue; + } + if (typeof k === 'boolean') { + sanitized.push({ tag: 'boolean', val: k }); + continue; + } + if (typeof k === 'bigint') { + sanitized.push({ tag: 'int64', val: k }); + continue; + } + if (typeof k === 'number') { + isFloat(k) + ? sanitized.push({ tag: 'floating64', val: k }) + : sanitized.push({ tag: 'int32', val: k }); + continue; + } + if ((k as any) instanceof Uint8Array) { + sanitized.push({ tag: 'binary', val: k }); + continue; + } + } + return sanitized; +} + +function isFloat(number: number) { + return number % 1 !== 0; +} + +function mapDbDataType(dbType: spinPg.DbDataType): PostgresV4DataType { + switch (dbType.tag) { + case 'boolean': return PostgresV4DataType.PostgresV4Boolean; + case 'int8': return PostgresV4DataType.PostgresV4Int8; + case 'int16': return PostgresV4DataType.PostgresV4Int16; + case 'int32': return PostgresV4DataType.PostgresV4Int32; + case 'int64': return PostgresV4DataType.PostgresV4Int64; + case 'floating32': return PostgresV4DataType.PostgresV4Floating32; + case 'floating64': return PostgresV4DataType.PostgresV4Floating64; + case 'str': return PostgresV4DataType.PostgresV4Str; + case 'binary': return PostgresV4DataType.PostgresV4Binary; + case 'date': return PostgresV4DataType.PostgresV4Date; + case 'time': return PostgresV4DataType.PostgresV4Time; + case 'datetime': return PostgresV4DataType.PostgresV4DateTime; + case 'timestamp': return PostgresV4DataType.PostgresV4TimeStamp; + case 'uuid': return PostgresV4DataType.PostgresV4Uuid; + case 'jsonb': return PostgresV4DataType.PostgresV4Jsonb; + case 'decimal': return PostgresV4DataType.PostgresV4Decimal; + case 'range-int32': return PostgresV4DataType.PostgresV4RangeInt32; + case 'range-int64': return PostgresV4DataType.PostgresV4RangeInt64; + case 'range-decimal': return PostgresV4DataType.PostgresV4RangeDecimal; + case 'array-int32': return PostgresV4DataType.PostgresV4ArrayInt32; + case 'array-int64': return PostgresV4DataType.PostgresV4ArrayInt64; + case 'array-decimal': return PostgresV4DataType.PostgresV4ArrayDecimal; + case 'array-str': return PostgresV4DataType.PostgresV4ArrayStr; + case 'interval': return PostgresV4DataType.PostgresV4Interval; + case 'other': return PostgresV4DataType.PostgresV4Other; + } +} + +function mapRowSet(rowSet: spinPg.RowSet): SpinRdbmsRowSet { + return { + columns: rowSet.columns.map(c => ({ + name: c.name, + dataType: mapDbDataType(c.dataType), + })), + rows: rowSet.rows as RdbmsRow[], // still compatible + }; +} \ No newline at end of file diff --git a/packages/spin-postgresv4/tsconfig.json b/packages/spin-postgresv4/tsconfig.json new file mode 100644 index 00000000..acfaf82e --- /dev/null +++ b/packages/spin-postgresv4/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "nodenext", + "lib": [ + "ESNext", + "WebWorker" + ], + "moduleResolution": "nodenext", + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/spin-postgresv4/types/spin-postgres-postgres.d.ts b/packages/spin-postgresv4/types/spin-postgres-postgres.d.ts new file mode 100644 index 00000000..cd6c2fab --- /dev/null +++ b/packages/spin-postgresv4/types/spin-postgres-postgres.d.ts @@ -0,0 +1,425 @@ +declare module 'spin:postgres/postgres@4.0.0' { + export interface DbError { + /** + * Stringised version of the error. This is primarily to facilitate migration of older code. + */ + asText: string, + severity: string, + code: string, + message: string, + detail?: string, + /** + * Any error information provided by Postgres and not captured above. + */ + extras: Array<[string, string]>, + } + export type QueryError = QueryErrorText | QueryErrorDbError; + /** + * An error occurred but we do not have structured info for it + */ + export interface QueryErrorText { + tag: 'text', + val: string, + } + /** + * Postgres returned a structured database error + */ + export interface QueryErrorDbError { + tag: 'db-error', + val: DbError, + } + /** + * Errors related to interacting with a database. + */ + export type Error = ErrorConnectionFailed | ErrorBadParameter | ErrorQueryFailed | ErrorValueConversionFailed | ErrorOther; + export interface ErrorConnectionFailed { + tag: 'connection-failed', + val: string, + } + export interface ErrorBadParameter { + tag: 'bad-parameter', + val: string, + } + export interface ErrorQueryFailed { + tag: 'query-failed', + val: QueryError, + } + export interface ErrorValueConversionFailed { + tag: 'value-conversion-failed', + val: string, + } + export interface ErrorOther { + tag: 'other', + val: string, + } + /** + * Data types for a database column + */ + export type DbDataType = DbDataTypeBoolean | DbDataTypeInt8 | DbDataTypeInt16 | DbDataTypeInt32 | DbDataTypeInt64 | DbDataTypeFloating32 | DbDataTypeFloating64 | DbDataTypeStr | DbDataTypeBinary | DbDataTypeDate | DbDataTypeTime | DbDataTypeDatetime | DbDataTypeTimestamp | DbDataTypeUuid | DbDataTypeJsonb | DbDataTypeDecimal | DbDataTypeRangeInt32 | DbDataTypeRangeInt64 | DbDataTypeRangeDecimal | DbDataTypeArrayInt32 | DbDataTypeArrayInt64 | DbDataTypeArrayDecimal | DbDataTypeArrayStr | DbDataTypeInterval | DbDataTypeOther; + export interface DbDataTypeBoolean { + tag: 'boolean', + } + export interface DbDataTypeInt8 { + tag: 'int8', + } + export interface DbDataTypeInt16 { + tag: 'int16', + } + export interface DbDataTypeInt32 { + tag: 'int32', + } + export interface DbDataTypeInt64 { + tag: 'int64', + } + export interface DbDataTypeFloating32 { + tag: 'floating32', + } + export interface DbDataTypeFloating64 { + tag: 'floating64', + } + export interface DbDataTypeStr { + tag: 'str', + } + export interface DbDataTypeBinary { + tag: 'binary', + } + export interface DbDataTypeDate { + tag: 'date', + } + export interface DbDataTypeTime { + tag: 'time', + } + export interface DbDataTypeDatetime { + tag: 'datetime', + } + export interface DbDataTypeTimestamp { + tag: 'timestamp', + } + export interface DbDataTypeUuid { + tag: 'uuid', + } + export interface DbDataTypeJsonb { + tag: 'jsonb', + } + export interface DbDataTypeDecimal { + tag: 'decimal', + } + export interface DbDataTypeRangeInt32 { + tag: 'range-int32', + } + export interface DbDataTypeRangeInt64 { + tag: 'range-int64', + } + export interface DbDataTypeRangeDecimal { + tag: 'range-decimal', + } + export interface DbDataTypeArrayInt32 { + tag: 'array-int32', + } + export interface DbDataTypeArrayInt64 { + tag: 'array-int64', + } + export interface DbDataTypeArrayDecimal { + tag: 'array-decimal', + } + export interface DbDataTypeArrayStr { + tag: 'array-str', + } + export interface DbDataTypeInterval { + tag: 'interval', + } + export interface DbDataTypeOther { + tag: 'other', + val: string, + } + export interface Interval { + micros: bigint, + days: number, + months: number, + } + /** + * A database column + */ + export interface Column { + name: string, + dataType: DbDataType, + } + /** + * For range types, indicates if each bound is inclusive or exclusive + * # Variants + * + * ## `"inclusive"` + * + * ## `"exclusive"` + */ + export type RangeBoundKind = 'inclusive' | 'exclusive'; + /** + * Database values + */ + export type DbValue = DbValueBoolean | DbValueInt8 | DbValueInt16 | DbValueInt32 | DbValueInt64 | DbValueFloating32 | DbValueFloating64 | DbValueStr | DbValueBinary | DbValueDate | DbValueTime | DbValueDatetime | DbValueTimestamp | DbValueUuid | DbValueJsonb | DbValueDecimal | DbValueRangeInt32 | DbValueRangeInt64 | DbValueRangeDecimal | DbValueArrayInt32 | DbValueArrayInt64 | DbValueArrayDecimal | DbValueArrayStr | DbValueInterval | DbValueDbNull | DbValueUnsupported; + export interface DbValueBoolean { + tag: 'boolean', + val: boolean, + } + export interface DbValueInt8 { + tag: 'int8', + val: number, + } + export interface DbValueInt16 { + tag: 'int16', + val: number, + } + export interface DbValueInt32 { + tag: 'int32', + val: number, + } + export interface DbValueInt64 { + tag: 'int64', + val: bigint, + } + export interface DbValueFloating32 { + tag: 'floating32', + val: number, + } + export interface DbValueFloating64 { + tag: 'floating64', + val: number, + } + export interface DbValueStr { + tag: 'str', + val: string, + } + export interface DbValueBinary { + tag: 'binary', + val: Uint8Array, + } + export interface DbValueDate { + tag: 'date', + val: [number, number, number], + } + /** + * (year, month, day) + */ + export interface DbValueTime { + tag: 'time', + val: [number, number, number, number], + } + /** + * (hour, minute, second, nanosecond) + * Date-time types are always treated as UTC (without timezone info). + * The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + */ + export interface DbValueDatetime { + tag: 'datetime', + val: [number, number, number, number, number, number, number], + } + /** + * Unix timestamp (seconds since epoch) + */ + export interface DbValueTimestamp { + tag: 'timestamp', + val: bigint, + } + export interface DbValueUuid { + tag: 'uuid', + val: string, + } + export interface DbValueJsonb { + tag: 'jsonb', + val: Uint8Array, + } + export interface DbValueDecimal { + tag: 'decimal', + val: string, + } + /** + * I admit defeat. Base 10 + */ + export interface DbValueRangeInt32 { + tag: 'range-int32', + val: [[number, RangeBoundKind] | undefined, [number, RangeBoundKind] | undefined], + } + export interface DbValueRangeInt64 { + tag: 'range-int64', + val: [[bigint, RangeBoundKind] | undefined, [bigint, RangeBoundKind] | undefined], + } + export interface DbValueRangeDecimal { + tag: 'range-decimal', + val: [[string, RangeBoundKind] | undefined, [string, RangeBoundKind] | undefined], + } + export interface DbValueArrayInt32 { + tag: 'array-int32', + val: Array, + } + export interface DbValueArrayInt64 { + tag: 'array-int64', + val: Array, + } + export interface DbValueArrayDecimal { + tag: 'array-decimal', + val: Array, + } + export interface DbValueArrayStr { + tag: 'array-str', + val: Array, + } + export interface DbValueInterval { + tag: 'interval', + val: Interval, + } + export interface DbValueDbNull { + tag: 'db-null', + } + export interface DbValueUnsupported { + tag: 'unsupported', + val: Uint8Array, + } + /** + * Values used in parameterized queries + */ + export type ParameterValue = ParameterValueBoolean | ParameterValueInt8 | ParameterValueInt16 | ParameterValueInt32 | ParameterValueInt64 | ParameterValueFloating32 | ParameterValueFloating64 | ParameterValueStr | ParameterValueBinary | ParameterValueDate | ParameterValueTime | ParameterValueDatetime | ParameterValueTimestamp | ParameterValueUuid | ParameterValueJsonb | ParameterValueDecimal | ParameterValueRangeInt32 | ParameterValueRangeInt64 | ParameterValueRangeDecimal | ParameterValueArrayInt32 | ParameterValueArrayInt64 | ParameterValueArrayDecimal | ParameterValueArrayStr | ParameterValueInterval | ParameterValueDbNull; + export interface ParameterValueBoolean { + tag: 'boolean', + val: boolean, + } + export interface ParameterValueInt8 { + tag: 'int8', + val: number, + } + export interface ParameterValueInt16 { + tag: 'int16', + val: number, + } + export interface ParameterValueInt32 { + tag: 'int32', + val: number, + } + export interface ParameterValueInt64 { + tag: 'int64', + val: bigint, + } + export interface ParameterValueFloating32 { + tag: 'floating32', + val: number, + } + export interface ParameterValueFloating64 { + tag: 'floating64', + val: number, + } + export interface ParameterValueStr { + tag: 'str', + val: string, + } + export interface ParameterValueBinary { + tag: 'binary', + val: Uint8Array, + } + export interface ParameterValueDate { + tag: 'date', + val: [number, number, number], + } + /** + * (year, month, day) + */ + export interface ParameterValueTime { + tag: 'time', + val: [number, number, number, number], + } + /** + * (hour, minute, second, nanosecond) + * Date-time types are always treated as UTC (without timezone info). + * The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + */ + export interface ParameterValueDatetime { + tag: 'datetime', + val: [number, number, number, number, number, number, number], + } + /** + * Unix timestamp (seconds since epoch) + */ + export interface ParameterValueTimestamp { + tag: 'timestamp', + val: bigint, + } + export interface ParameterValueUuid { + tag: 'uuid', + val: string, + } + export interface ParameterValueJsonb { + tag: 'jsonb', + val: Uint8Array, + } + export interface ParameterValueDecimal { + tag: 'decimal', + val: string, + } + /** + * base 10 + */ + export interface ParameterValueRangeInt32 { + tag: 'range-int32', + val: [[number, RangeBoundKind] | undefined, [number, RangeBoundKind] | undefined], + } + export interface ParameterValueRangeInt64 { + tag: 'range-int64', + val: [[bigint, RangeBoundKind] | undefined, [bigint, RangeBoundKind] | undefined], + } + export interface ParameterValueRangeDecimal { + tag: 'range-decimal', + val: [[string, RangeBoundKind] | undefined, [string, RangeBoundKind] | undefined], + } + export interface ParameterValueArrayInt32 { + tag: 'array-int32', + val: Array, + } + export interface ParameterValueArrayInt64 { + tag: 'array-int64', + val: Array, + } + export interface ParameterValueArrayDecimal { + tag: 'array-decimal', + val: Array, + } + export interface ParameterValueArrayStr { + tag: 'array-str', + val: Array, + } + export interface ParameterValueInterval { + tag: 'interval', + val: Interval, + } + export interface ParameterValueDbNull { + tag: 'db-null', + } + /** + * A database row + */ + export type Row = Array; + /** + * A set of database rows + */ + export interface RowSet { + columns: Array, + rows: Array, + } + + export class Connection implements Disposable { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Open a connection to the Postgres instance at `address`. + */ + static open(address: string): Connection; + /** + * Query the database. + */ + query(statement: string, params: Array): RowSet; + /** + * Execute command to the database. + */ + execute(statement: string, params: Array): bigint; + [Symbol.dispose](): void; + } +} diff --git a/packages/spin-postgresv4/wit/world.wit b/packages/spin-postgresv4/wit/world.wit new file mode 100644 index 00000000..29ec90f5 --- /dev/null +++ b/packages/spin-postgresv4/wit/world.wit @@ -0,0 +1,167 @@ +package spin:postgres@4.0.0; + +interface postgres { + /// Errors related to interacting with a database. + variant error { + connection-failed(string), + bad-parameter(string), + query-failed(query-error), + value-conversion-failed(string), + other(string) + } + + variant query-error { + /// An error occurred but we do not have structured info for it + text(string), + /// Postgres returned a structured database error + db-error(db-error), + } + + record db-error { + /// Stringised version of the error. This is primarily to facilitate migration of older code. + as-text: string, + severity: string, + code: string, + message: string, + detail: option, + /// Any error information provided by Postgres and not captured above. + extras: list>, + } + + /// Data types for a database column + variant db-data-type { + boolean, + int8, + int16, + int32, + int64, + floating32, + floating64, + str, + binary, + date, + time, + datetime, + timestamp, + uuid, + jsonb, + decimal, + range-int32, + range-int64, + range-decimal, + array-int32, + array-int64, + array-decimal, + array-str, + interval, + other(string), + } + + /// Database values + variant db-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + floating32(f32), + floating64(f64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + /// Date-time types are always treated as UTC (without timezone info). + /// The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + datetime(tuple), + /// Unix timestamp (seconds since epoch) + timestamp(s64), + uuid(string), + jsonb(list), + decimal(string), // I admit defeat. Base 10 + range-int32(tuple>, option>>), + range-int64(tuple>, option>>), + range-decimal(tuple>, option>>), + array-int32(list>), + array-int64(list>), + array-decimal(list>), + array-str(list>), + interval(interval), + db-null, + unsupported(list), + } + + /// Values used in parameterized queries + variant parameter-value { + boolean(bool), + int8(s8), + int16(s16), + int32(s32), + int64(s64), + floating32(f32), + floating64(f64), + str(string), + binary(list), + date(tuple), // (year, month, day) + time(tuple), // (hour, minute, second, nanosecond) + /// Date-time types are always treated as UTC (without timezone info). + /// The instant is represented as a (year, month, day, hour, minute, second, nanosecond) tuple. + datetime(tuple), + /// Unix timestamp (seconds since epoch) + timestamp(s64), + uuid(string), + jsonb(list), + decimal(string), // base 10 + range-int32(tuple>, option>>), + range-int64(tuple>, option>>), + range-decimal(tuple>, option>>), + array-int32(list>), + array-int64(list>), + array-decimal(list>), + array-str(list>), + interval(interval), + db-null, + } + + record interval { + micros: s64, + days: s32, + months: s32, + } + + /// A database column + record column { + name: string, + data-type: db-data-type, + } + + /// A database row + type row = list; + + /// A set of database rows + record row-set { + columns: list, + rows: list, + } + + /// For range types, indicates if each bound is inclusive or exclusive + enum range-bound-kind { + inclusive, + exclusive, + } + + /// A connection to a postgres database. + resource connection { + /// Open a connection to the Postgres instance at `address`. + open: static func(address: string) -> result; + + /// Query the database. + query: func(statement: string, params: list) -> result; + + /// Execute command to the database. + execute: func(statement: string, params: list) -> result; + } +} + +world spin-postgres4 { + import postgres; +} \ No newline at end of file