diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6136f6e..5d14909 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,7 @@ jobs: - name: Setup TinyGo uses: acifani/setup-tinygo@v1 with: - tinygo-version: '0.27.0' + tinygo-version: '0.29.0' - name: Version 👍 id: version-bump diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e80320..141c352 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: test: strategy: matrix: - go-version: [1.18.x, v1.19.x] + go-version: [1.18.x, v1.19.x, v1.20.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: @@ -70,7 +70,7 @@ jobs: - name: Setup 🐹 uses: acifani/setup-tinygo@v1 with: - tinygo-version: 0.27.0 + tinygo-version: 0.29.0 - name: Install 🔧 run: npm install diff --git a/.gitignore b/.gitignore index 600d2d3..4c965a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.vscode \ No newline at end of file +.vscode +.DS_Store +diagrams* +s2ts_gen_*.go \ No newline at end of file diff --git a/Makefile b/Makefile index f4a4f10..3ce7877 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,23 @@ generate: # requires java generate-diagrams: go run ebnf/main.go grammar.y | java -jar rr/rr.war -suppressebnf -color:#FFFFFF -out:diagrams.xhtml - -.PHONY: generate-diagrams \ No newline at end of file +.PHONY: generate-diagrams + + +# go get -u -v github.com/OneOfOne/struct2ts/... +types: + struct2ts --interface --no-helpers \ + sqlparser.CreateTable \ + sqlparser.ColumnConstraintPrimaryKey \ + sqlparser.ColumnConstraintNotNull \ + sqlparser.ColumnConstraintUnique \ + sqlparser.ColumnConstraintCheck \ + sqlparser.ColumnConstraintDefault \ + sqlparser.ColumnConstraintGenerated \ + sqlparser.TableConstraintPrimaryKey \ + sqlparser.TableConstraintUnique \ + sqlparser.TableConstraintCheck \ + > js/go-types.d.ts + echo "export type ColumnConstraint = ColumnConstraintPrimaryKey | ColumnConstraintNotNull | ColumnConstraintUnique | ColumnConstraintCheck | ColumnConstraintDefault | ColumnConstraintGenerated & { Type: string };" >> js/go-types.d.ts + echo "export type TableConstraint = TableConstraintPrimaryKey | TableConstraintUnique | TableConstraintCheck;" >> js/go-types.d.ts +.PHONY: types \ No newline at end of file diff --git a/ast.go b/ast.go index dd7ebee..3491bd4 100644 --- a/ast.go +++ b/ast.go @@ -3,6 +3,7 @@ package sqlparser import ( "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" "sort" @@ -1465,6 +1466,46 @@ func (node *CreateTable) StructureHash() string { return hex.EncodeToString(hash) } +// UnmarshalJSON implements the json.Marshaler interface. +func (node *CreateTable) UnmarshalJSON(data []byte) error { + var v struct { + Table *Table + ColumnsDef []*ColumnDef + Constraints []json.RawMessage + StrictMode bool + } + if err := json.Unmarshal(data, &v); err != nil { + return err + } + node.Table = v.Table + node.StrictMode = v.StrictMode + node.ColumnsDef = v.ColumnsDef + for _, constraintData := range v.Constraints { + var constraintType struct { + Type string + } + if err := json.Unmarshal(constraintData, &constraintType); err != nil { + return err + } + + var constraint TableConstraint + switch constraintType.Type { + case "primary-key": + constraint = &TableConstraintPrimaryKey{} + case "unique": + constraint = &TableConstraintUnique{} + default: + return fmt.Errorf("unable to process constraint type: %s", constraintType.Type) + } + + if err := json.Unmarshal(constraintData, constraint); err != nil { + return err + } + node.Constraints = append(node.Constraints, constraint) + } + return nil +} + // ColumnDef represents the column definition of a CREATE TABLE statement. type ColumnDef struct { Column *Column @@ -1514,6 +1555,46 @@ func (node *ColumnDef) HasPrimaryKey() bool { return false } +// UnmarshalJSON implements the json.Marshaler interface. +func (node *ColumnDef) UnmarshalJSON(data []byte) error { + var v struct { + Column *Column + Type string + Constraints []json.RawMessage + } + if err := json.Unmarshal(data, &v); err != nil { + return err + } + node.Type = v.Type + node.Column = v.Column + for _, constraintData := range v.Constraints { + var constraintType struct { + Type string + } + if err := json.Unmarshal(constraintData, &constraintType); err != nil { + return err + } + + var constraint ColumnConstraint + switch constraintType.Type { + case "primary-key": + constraint = &ColumnConstraintPrimaryKey{} + case "not-null": + constraint = &ColumnConstraintNotNull{} + case "unique": + constraint = &ColumnConstraintUnique{} + default: + return fmt.Errorf("unable to process constraint type: %s", constraintType.Type) + } + + if err := json.Unmarshal(constraintData, constraint); err != nil { + return err + } + node.Constraints = append(node.Constraints, constraint) + } + return nil +} + // Types for ColumnDef type. const ( TypeIntStr = "int" @@ -1580,6 +1661,35 @@ const ( PrimaryKeyOrderDesc = "desc" ) +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintPrimaryKey) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintPrimaryKey + }{ + Type: "primary-key", + ColumnConstraintPrimaryKey: ColumnConstraintPrimaryKey{ + Name: node.Name, + Order: node.Order, + AutoIncrement: node.AutoIncrement, + }, + }) +} + +// func (c *ColumnConstraintPrimaryKey) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintPrimaryKey +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// c.Order = v.Order +// c.AutoIncrement = v.AutoIncrement +// return nil +// } + // ColumnConstraintNotNull represents a NOT NULL column constraint for CREATE TABLE. type ColumnConstraintNotNull struct { Name Identifier @@ -1603,6 +1713,31 @@ func (node *ColumnConstraintNotNull) walkSubtree(visit Visit) error { return Walk(visit, node.Name) } +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintNotNull) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintNotNull + }{ + Type: "not-null", + ColumnConstraintNotNull: ColumnConstraintNotNull{ + Name: node.Name, + }, + }) +} + +// func (c *ColumnConstraintNotNull) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintNotNull +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// return nil +// } + // ColumnConstraintUnique represents a UNIQUE column constraint for CREATE TABLE. type ColumnConstraintUnique struct { Name Identifier @@ -1626,6 +1761,31 @@ func (node *ColumnConstraintUnique) walkSubtree(visit Visit) error { return Walk(visit, node.Name) } +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintUnique) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintUnique + }{ + Type: "unique", + ColumnConstraintUnique: ColumnConstraintUnique{ + Name: node.Name, + }, + }) +} + +// func (c *ColumnConstraintUnique) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintUnique +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// return nil +// } + // ColumnConstraintCheck represents a CHECK column constraint for CREATE TABLE. type ColumnConstraintCheck struct { Name Identifier @@ -1649,6 +1809,33 @@ func (node *ColumnConstraintCheck) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Expr) } +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintCheck + }{ + Type: "check", + ColumnConstraintCheck: ColumnConstraintCheck{ + Name: node.Name, + Expr: node.Expr, + }, + }) +} + +// func (c *ColumnConstraintCheck) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintCheck +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// c.Expr = v.Expr +// return nil +// } + // ColumnConstraintDefault represents a DEFAULT column constraint for CREATE TABLE. type ColumnConstraintDefault struct { Name Identifier @@ -1676,6 +1863,35 @@ func (node *ColumnConstraintDefault) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Expr) } +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintDefault) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintDefault + }{ + Type: "default", + ColumnConstraintDefault: ColumnConstraintDefault{ + Name: node.Name, + Expr: node.Expr, + Parenthesis: node.Parenthesis, + }, + }) +} + +// func (c *ColumnConstraintDefault) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintDefault +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// c.Expr = v.Expr +// c.Parenthesis = v.Parenthesis +// return nil +// } + // ColumnConstraintGenerated represents a GENERATED ALWAYS column constraint for CREATE TABLE. type ColumnConstraintGenerated struct { Name Identifier @@ -1717,6 +1933,37 @@ func (node *ColumnConstraintGenerated) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Expr) } +// MarshalJSON implements the json.Marshaler interface. +func (node *ColumnConstraintGenerated) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + ColumnConstraintGenerated + }{ + Type: "generated", + ColumnConstraintGenerated: ColumnConstraintGenerated{ + Name: node.Name, + Expr: node.Expr, + GeneratedAlways: node.GeneratedAlways, + IsStored: node.IsStored, + }, + }) +} + +// func (c *ColumnConstraintGenerated) UnmarshalJSON(data []byte) error { +// var v struct { +// Type string +// ColumnConstraintGenerated +// } +// if err := json.Unmarshal(data, &v); err != nil { +// return err +// } +// c.Name = v.Name +// c.Expr = v.Expr +// c.GeneratedAlways = v.GeneratedAlways +// c.IsStored = v.IsStored +// return nil +// } + // TableConstraint is a contrainst applied to the whole table in a CREATE TABLE statement. type TableConstraint interface { iTableConstraint() @@ -1751,6 +1998,20 @@ func (node *TableConstraintPrimaryKey) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Columns) } +// MarshalJSON implements the json.Marshaler interface. +func (node *TableConstraintPrimaryKey) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + TableConstraintPrimaryKey + }{ + Type: "primary-key", + TableConstraintPrimaryKey: TableConstraintPrimaryKey{ + Name: node.Name, + Columns: node.Columns, + }, + }) +} + // TableConstraintUnique is a UNIQUE constraint for table definition. type TableConstraintUnique struct { Name Identifier @@ -1775,6 +2036,20 @@ func (node *TableConstraintUnique) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Columns) } +// MarshalJSON implements the json.Marshaler interface. +func (node *TableConstraintUnique) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + TableConstraintUnique + }{ + Type: "unique", + TableConstraintUnique: TableConstraintUnique{ + Name: node.Name, + Columns: node.Columns, + }, + }) +} + // TableConstraintCheck is a CHECK constraint for table definition. type TableConstraintCheck struct { Name Identifier @@ -1799,6 +2074,20 @@ func (node *TableConstraintCheck) walkSubtree(visit Visit) error { return Walk(visit, node.Name, node.Expr) } +// MarshalJSON implements the json.Marshaler interface. +func (node *TableConstraintCheck) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + TableConstraintCheck + }{ + Type: "check", + TableConstraintCheck: TableConstraintCheck{ + Name: node.Name, + Expr: node.Expr, + }, + }) +} + // Insert represents an INSERT statement. type Insert struct { Table *Table diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go index f6ed709..a0e20b5 100644 --- a/cmd/wasm/main.go +++ b/cmd/wasm/main.go @@ -2,6 +2,7 @@ package main import ( + "encoding/json" "regexp" "strings" "syscall/js" @@ -30,6 +31,22 @@ type EnclosingType struct { close string } +var ( + Console js.Value + JSON js.Value + Error js.Value + TypeError js.Value + Promise js.Value +) + +func init() { + Console = js.Global().Get("console") + TypeError = js.Global().Get("TypeError") + Error = js.Global().Get("Error") + Promise = js.Global().Get("Promise") + JSON = js.Global().Get("JSON") +} + func getEnclosures() []EnclosingType { return []EnclosingType{ {open: "`", close: "`"}, @@ -65,9 +82,65 @@ func UpdateTableNames(node sqlparser.Node, nameMapper func(string) (string, bool return node, nil } +func createStatementFromObject(this js.Value, args []js.Value) interface{} { + if len(args) < 1 { + return Promise.Call("reject", Error.New("missing required argument: ast")) + } + astObject := args[0] + if astObject.Type() != js.TypeObject { + return Promise.Call("reject", TypeError.New("invalid argument: object expected")) + } + handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + resolve := args[0] + reject := args[1] + go func() interface{} { + jsonString := JSON.Call("stringify", astObject).String() + var create sqlparser.CreateTable + if err := json.Unmarshal([]byte(jsonString), &create); err != nil { + return reject.Invoke(Error.New("Error unmarshaling into struct: " + err.Error())) + } + var response interface{} = create.String() + return resolve.Invoke(js.ValueOf(response)) + }() + return nil + }) + return Promise.New(handler) +} + +func createStatementToObject(this js.Value, args []js.Value) interface{} { + if len(args) < 1 { + return Promise.Call("reject", Error.New("missing required argument: statement")) + } + statement := args[0].String() + handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + resolve := args[0] + reject := args[1] + go func() interface{} { + ast, err := sqlparser.Parse(statement) + if err != nil { + return reject.Invoke(Error.New("error parsing statement: " + err.Error())) + } + if len(ast.Statements) == 0 { + return reject.Invoke(Error.New("error parsing statement: empty string")) + } + if len(ast.Statements) > 1 { + return reject.Invoke(Error.New("expected single create statement")) + } + create, ok := ast.Statements[0].(sqlparser.CreateTableStatement) + if !ok { + return reject.Invoke(Error.New("expected single create statement")) + } + b, _ := json.Marshal(&create) + var response map[string]interface{} + _ = json.Unmarshal(b, &response) + return resolve.Invoke(js.ValueOf(response)) + }() + return nil + }) + return Promise.New(handler) +} + func validateTableName(this js.Value, args []js.Value) interface{} { - Error := js.Global().Get("Error") - Promise := js.Global().Get("Promise") if len(args) < 1 { return Promise.Call("reject", Error.New("missing required argument: tableName")) } @@ -108,8 +181,6 @@ func validateTableName(this js.Value, args []js.Value) interface{} { } func getUniqueTableNames(this js.Value, args []js.Value) interface{} { - Error := js.Global().Get("Error") - Promise := js.Global().Get("Promise") if len(args) < 1 { return Promise.Call("reject", Error.New("missing required argument: statement")) } @@ -135,8 +206,6 @@ func getUniqueTableNames(this js.Value, args []js.Value) interface{} { } func normalize(this js.Value, args []js.Value) interface{} { - Error := js.Global().Get("Error") - Promise := js.Global().Get("Promise") if len(args) < 1 { return Promise.Call("reject", Error.New("missing required argument: statement")) } @@ -236,9 +305,11 @@ func getEnclosedName(name string) (string, EnclosingType, bool) { func main() { // Outer object is exported globally and contains these keys js.Global().Set(GLOBAL_NAME, js.ValueOf(map[string]interface{}{ - "normalize": js.FuncOf(normalize), - "validateTableName": js.FuncOf(validateTableName), - "getUniqueTableNames": js.FuncOf(getUniqueTableNames), + "normalize": js.FuncOf(normalize), + "validateTableName": js.FuncOf(validateTableName), + "getUniqueTableNames": js.FuncOf(getUniqueTableNames), + "createStatementFromObject": js.FuncOf(createStatementFromObject), + "createStatementToObject": js.FuncOf(createStatementToObject), })) <-make(chan bool) diff --git a/go.mod b/go.mod index 42dcd39..18c67ea 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,12 @@ require ( ) require ( + github.com/OneOfOne/struct2ts v1.0.6 // indirect + github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect + github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/tools v0.0.0-20190213135902-6bedcd10978a // indirect + gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d0a3fe9..93d2f42 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/OneOfOne/struct2ts v1.0.6 h1:kLFEisG4K43k1thctN9BNZP4Y/RxRbO8tPyi3G5qPl4= +github.com/OneOfOne/struct2ts v1.0.6/go.mod h1:GbIenlFXroS2wRhpYXHEq7y7HWsY3SFBIKxkqzbnAsU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,10 +20,15 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/tools v0.0.0-20190213135902-6bedcd10978a h1:ncPOGSo3avrTTUKHvDmwoS5E5of95qqNwftSXoxX+Wk= +golang.org/x/tools v0.0.0-20190213135902-6bedcd10978a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers_test.go b/helpers_test.go index 0c431dd..1e13f8d 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -1,6 +1,7 @@ package sqlparser import ( + "encoding/json" "fmt" "testing" @@ -155,3 +156,42 @@ func TestWalk(t *testing.T) { require.NoError(t, err) }) } + +func TestJsonMarshalling(t *testing.T) { + t.Parallel() + + t.Run("roundtrip", func(t *testing.T) { + t.Parallel() + + sql := "CREATE TABLE t (id INT CONSTRAINT nm NOT NULL, id2 INT, CONSTRAINT pk PRIMARY KEY (id), CONSTRAINT un UNIQUE (id, id2));" // nolint + ast, err := Parse(sql) + require.NoError(t, err) + create, ok := ast.Statements[0].(CreateTableStatement) + require.True(t, ok) + table, ok := create.(*CreateTable) + require.True(t, ok) + b, err := json.Marshal(&table) + require.NoError(t, err) + var output CreateTable + err = json.Unmarshal(b, &output) + require.NoError(t, err) + require.Equal(t, ast.String(), output.String()) + }) + + t.Run("limited types on unmarshall", func(t *testing.T) { + t.Parallel() + + sql := "CREATE TABLE t (a INT CONSTRAINT default_constraint DEFAULT 0, b INT DEFAULT 1, c INT DEFAULT 0x1, d TEXT DEFAULT 'foo', e TEXT DEFAULT ('foo'), f INT DEFAULT +1);" // nolint + ast, err := Parse(sql) + require.NoError(t, err) + create, ok := ast.Statements[0].(CreateTableStatement) + require.True(t, ok) + table, ok := create.(*CreateTable) + require.True(t, ok) + b, err := json.Marshal(&table) + require.NoError(t, err) + var output CreateTable + err = json.Unmarshal(b, &output) + require.Error(t, err) + }) +} diff --git a/js/.eslintrc.json b/js/.eslintrc.json index 277300f..ea644cf 100644 --- a/js/.eslintrc.json +++ b/js/.eslintrc.json @@ -12,6 +12,7 @@ "ecmaVersion": 12 }, "rules": { - "import/order": "warn" + "import/order": "warn", + "no-use-before-define": "off" } } diff --git a/js/README.md b/js/README.md index 857b4a1..4b77fd2 100644 --- a/js/README.md +++ b/js/README.md @@ -72,6 +72,8 @@ To get started clone this repo. ## Install tinygo +We require tinygo version `0.28.1` or greater + ``` brew tap tinygo-org/tools brew install tinygo @@ -79,10 +81,11 @@ brew install tinygo ## Fetch wasm helpers -Use the corresponding tinygo version +Use the corresponding tinygo version. +**Warning** this will overwrite any existing `wasm_exec.js` file, which has Tableland specific modifications. ``` -wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.23.0/targets/wasm_exec.js +wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.29.0/targets/wasm_exec.js ``` ## Build with tinygo @@ -92,10 +95,20 @@ tinygo build -gc=leaking -no-debug -o main.wasm -target wasm ./main.go wasm-opt -O main.wasm -o main.wasm ``` +## Generate types + +From the top-level directory: + +``` +go get github.com/OneOfOne/struct2ts/... +make types +``` + or use the build scripts: ``` npm install +npm run build:go-types npm run build ``` diff --git a/js/go-types.d.ts b/js/go-types.d.ts new file mode 100644 index 0000000..6e73710 --- /dev/null +++ b/js/go-types.d.ts @@ -0,0 +1,103 @@ +// this file was automatically generated, DO NOT EDIT +// structs +// struct2ts:github.com/tablelandnetwork/sqlparser.Table +export interface Table { + Name: string; + IsTarget: boolean; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.Column +export interface Column { + Name: string; + TableRef: Table | null; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnDef +export interface ColumnDef { + Column: Column | null; + Type: string; + Constraints: ColumnConstraint[] | null; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.CreateTable +export interface CreateTable { + Table: Table | null; + ColumnsDef: ColumnDef[] | null; + Constraints: TableConstraint[] | null; + StrictMode: boolean; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintPrimaryKey +export interface ColumnConstraintPrimaryKey { + Name: string; + Order: string; + AutoIncrement: boolean; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintNotNull +export interface ColumnConstraintNotNull { + Name: string; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintUnique +export interface ColumnConstraintUnique { + Name: string; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintCheck +export interface ColumnConstraintCheck { + Name: string; + Expr: any; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintDefault +export interface ColumnConstraintDefault { + Name: string; + Expr: any; + Parenthesis: boolean; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.ColumnConstraintGenerated +export interface ColumnConstraintGenerated { + Name: string; + Expr: any; + GeneratedAlways: boolean; + IsStored: boolean; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.IndexedColumn +export interface IndexedColumn { + Column: Column | null; + CollationName: string; + Order: string; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.TableConstraintPrimaryKey +export interface TableConstraintPrimaryKey { + Name: string; + Columns: IndexedColumn[] | null; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.TableConstraintUnique +export interface TableConstraintUnique { + Name: string; + Columns: Column[] | null; +} + +// struct2ts:github.com/tablelandnetwork/sqlparser.TableConstraintCheck +export interface TableConstraintCheck { + Name: string; + Expr: any; +} + +export type ColumnConstraint = + | ColumnConstraintPrimaryKey + | ColumnConstraintNotNull + | ColumnConstraintUnique + | ColumnConstraintCheck + | ColumnConstraintDefault + | (ColumnConstraintGenerated & { Type: string }); +export type TableConstraint = + | TableConstraintPrimaryKey + | TableConstraintUnique + | TableConstraintCheck; diff --git a/js/main.js b/js/main.js index 0ec89d2..5a36d64 100644 --- a/js/main.js +++ b/js/main.js @@ -1,10 +1,11 @@ +// @ts-check +/* global Go */ + // Need to optionally shim `crypto.getRandomValues` and esbuild needs the // import to come before importing `wasm_exec.js` import "./polyfills/crypto.js"; - -// @ts-check -/* global Go */ import "./wasm_exec.js"; +// @ts-ignore import mainWasm from "./main.wasm"; // @ts-ignore diff --git a/js/main.wasm b/js/main.wasm index d8aa0f3..6bfaca7 100755 Binary files a/js/main.wasm and b/js/main.wasm differ diff --git a/js/package.json b/js/package.json index 22b8f44..634d655 100644 --- a/js/package.json +++ b/js/package.json @@ -52,11 +52,12 @@ "wasm:go": "tinygo build -gc=leaking -no-debug -o main.wasm -target wasm ../cmd/wasm/main.go", "wasm:opt": "npx wasm-opt -O main.wasm -o main.wasm", "fixup": "echo '{\n \"type\": \"commonjs\"\n}' > cjs/package.json", + "build:go-types": "cd .. && make types && cd js && npm run prettier -- --write go-types.d.ts", "build:cjs": "node ./cjs-build.js && npm run fixup", "build:esm": "node ./esm-build.js", "build:wasm": "npm run wasm:go && npm run wasm:opt", "build": "npm run build:wasm && npm run build:cjs && npm run build:esm", - "clean": "rm -rf main.wasm cjs", + "clean": "rm -rf main.wasm cjs go-types.d.ts", "prepublishOnly": "npm run build" }, "tsd": { diff --git a/js/test/example.html b/js/test/example.html index c33e28c..e2f769d 100644 --- a/js/test/example.html +++ b/js/test/example.html @@ -1,26 +1,29 @@ - - - - -

- SELECT * from blah WHERE id = 1; -

- - - + + + + + + +

+ SELECT * from blah WHERE id = 1; +

+ + + + \ No newline at end of file diff --git a/js/test/main.test.cjs b/js/test/main.test.cjs index 7edc3fc..2880179 100644 --- a/js/test/main.test.cjs +++ b/js/test/main.test.cjs @@ -339,6 +339,183 @@ describe("sqlparser", function () { }); }); + describe("to and from create statements and objects", function() { + test("round trip", async function() { + const statement = "create table blah_5_(id int,image blob,description text)" + const obj = await globalThis.sqlparser.createStatementToObject(statement); + const sql = await globalThis.sqlparser.createStatementFromObject(obj); + strictEqual(sql, statement); + }); + + test("from create statement", async function() { + const statement = "CREATE TABLE t (id INT CONSTRAINT nm NOT NULL, id2 INT, CONSTRAINT pk PRIMARY KEY (id), CONSTRAINT un UNIQUE (id, id2));" + const obj = await globalThis.sqlparser.createStatementToObject(statement); + const sql = await globalThis.sqlparser.createStatementFromObject(obj); + const { statements } = await globalThis.sqlparser.normalize(sql); + strictEqual(sql, statements[0]); + }) + + test("from object", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_30001(id int)"; + strictEqual(observed, expected); + }); + + test("from object with a constraint", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [{ + Type: "primary-key", + Name: "id", + Order: "asc", + AutoIncrement: false, + }] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_30001(id int constraint id primary key asc)"; + strictEqual(observed, expected); + }); + + test("from object with ambiguous constraints", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [{ + Type: "not-null", + "Name": "", + }, + { + Type: "unique", + "Name": "", + }] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_(id int not null unique)"; + strictEqual(observed, expected); + }); + + test("from schema object to create table to ast object", async function() { + /** + * + * @param {string} tableName + * @param {any} schema + * @returns {string} The CREATE statement string + */ + function generateCreateTableStatement(tableName, schema) { + const columnDefinitions = schema.columns.map((/** @type {{ name: string; type: string; constraints: string[]; }} */ column) => { + const definition = `${column.name} ${column.type}`; + const columnConstraints = column.constraints ? " " + column.constraints.join(' ') : ''; + return `${definition}${columnConstraints.toLowerCase()}`; + }).join(','); + + const tableConstraints = schema.tableConstraints ? schema.tableConstraints.join(',') : ''; + + return `create table ${tableName}(${columnDefinitions}${tableConstraints ? `, ${tableConstraints}` : ''})`; + } + + const columns = [ + { type: "INTEGER", name: 'a', constraints: ['CONSTRAINT pk PRIMARY KEY'] }, + { type: "INT", name: 'b'}, + { type: "TEXT", name: 'c'}, + ] + /** @type {any} */ + const tableConstraints = [] + const createTableStatement = generateCreateTableStatement('t', { columns, tableConstraints }) + const intermediate = await globalThis.sqlparser.createStatementToObject(createTableStatement); + console.log(JSON.stringify(intermediate, null, 2)) + const final = await globalThis.sqlparser.createStatementFromObject(intermediate); + const { statements } = await globalThis.sqlparser.normalize(createTableStatement); + const expected = "create table t(a integer constraint pk primary key autoincrement,b int,c text)"; + strictEqual(statements[0], expected); + strictEqual(final, expected); + }); + + test("to object", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const expected = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementToObject("create table blah_5_30001(id int)"); + deepStrictEqual(observed, expected); + }); + + test("with an error on valid statement", async function() { + await rejects( + globalThis.sqlparser.createStatementToObject("insert INTO blah_5_ values (1, 'three', 'something');"), + (/** @type {any} */ err) => { + strictEqual( + err.message, + "expected single create statement" + ); + return true; + } + ); + }); + }) + describe("validateTableName()", function () { test("when provided with invalid table names", async function () { const invalidNames = [ diff --git a/js/test/main.test.js b/js/test/main.test.js index 8a09ed2..310297e 100644 --- a/js/test/main.test.js +++ b/js/test/main.test.js @@ -344,6 +344,183 @@ describe("sqlparser", function () { }); }); + describe("to and from create statements and objects", function() { + test("round trip", async function() { + const statement = "create table blah_5_(id int,image blob,description text)" + const obj = await globalThis.sqlparser.createStatementToObject(statement); + const sql = await globalThis.sqlparser.createStatementFromObject(obj); + strictEqual(sql, statement); + }); + + test("from create statement", async function() { + const statement = "CREATE TABLE t (id INT CONSTRAINT nm NOT NULL, id2 INT, CONSTRAINT pk PRIMARY KEY (id), CONSTRAINT un UNIQUE (id, id2));" + const obj = await globalThis.sqlparser.createStatementToObject(statement); + const sql = await globalThis.sqlparser.createStatementFromObject(obj); + const { statements } = await globalThis.sqlparser.normalize(sql); + strictEqual(sql, statements[0]); + }) + + test("from object", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_30001(id int)"; + strictEqual(observed, expected); + }); + + test("from object with a constraint", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [{ + Type: "primary-key", + Name: "id", + Order: "asc", + AutoIncrement: false, + }] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_30001(id int constraint id primary key asc)"; + strictEqual(observed, expected); + }); + + test("from object with ambiguous constraints", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const obj = { + Table: { + Name: "blah_5_", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [{ + Type: "not-null", + "Name": "", + }, + { + Type: "unique", + "Name": "", + }] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementFromObject(obj); + const expected = "create table blah_5_(id int not null unique)"; + strictEqual(observed, expected); + }); + + test("from schema object to create table to ast object", async function() { + /** + * + * @param {string} tableName + * @param {any} schema + * @returns {string} The CREATE statement string + */ + function generateCreateTableStatement(tableName, schema) { + const columnDefinitions = schema.columns.map((/** @type {{ name: string; type: string; constraints: string[]; }} */ column) => { + const definition = `${column.name} ${column.type}`; + const columnConstraints = column.constraints ? " " + column.constraints.join(' ') : ''; + return `${definition}${columnConstraints.toLowerCase()}`; + }).join(','); + + const tableConstraints = schema.tableConstraints ? schema.tableConstraints.join(',') : ''; + + return `create table ${tableName}(${columnDefinitions}${tableConstraints ? `, ${tableConstraints}` : ''})`; + } + + const columns = [ + { type: "INTEGER", name: 'a', constraints: ['CONSTRAINT pk PRIMARY KEY'] }, + { type: "INT", name: 'b'}, + { type: "TEXT", name: 'c'}, + ] + /** @type {any} */ + const tableConstraints = [] + const createTableStatement = generateCreateTableStatement('t', { columns, tableConstraints }) + const intermediate = await globalThis.sqlparser.createStatementToObject(createTableStatement); + console.log(JSON.stringify(intermediate, null, 2)) + const final = await globalThis.sqlparser.createStatementFromObject(intermediate); + const { statements } = await globalThis.sqlparser.normalize(createTableStatement); + const expected = "create table t(a integer constraint pk primary key autoincrement,b int,c text)"; + strictEqual(statements[0], expected); + strictEqual(final, expected); + }); + + test("to object", async function() { + /** @type {import("@tableland/sqlparser").CreateTable } */ + const expected = { + Table: { + Name: "blah_5_30001", + IsTarget: true + }, + ColumnsDef: [ + { + Column: { + Name: "id", + TableRef: null, + }, + Type: "int", + Constraints: [] + } + ], + Constraints: [], + StrictMode: false + }; + const observed = await globalThis.sqlparser.createStatementToObject("create table blah_5_30001(id int)"); + deepStrictEqual(observed, expected); + }); + + test("with an error on valid statement", async function() { + await rejects( + globalThis.sqlparser.createStatementToObject("insert INTO blah_5_ values (1, 'three', 'something');"), + (/** @type {any} */ err) => { + strictEqual( + err.message, + "expected single create statement" + ); + return true; + } + ); + }); + }) + describe("validateTableName()", function () { test("when provided with invalid table names", async function () { const invalidNames = [ diff --git a/js/test/test-d.ts b/js/test/test-d.ts index 4b13296..95a998d 100644 --- a/js/test/test-d.ts +++ b/js/test/test-d.ts @@ -8,6 +8,7 @@ import defaultInit, { NormalizedStatement, ValidatedTable, StatementType, + CreateTable, } from "@tableland/sqlparser"; expectType>(defaultInit()); @@ -21,8 +22,25 @@ expectType>( }) ); -const { normalize, validateTableName, getUniqueTableNames } = - globalThis.sqlparser; +const { + normalize, + validateTableName, + getUniqueTableNames, + createStatementToObject, + createStatementFromObject, +} = globalThis.sqlparser; + +expectType>( + createStatementToObject("select * from table where id = 1;") +); +expectType>( + createStatementFromObject({ + Table: { Name: "table", IsTarget: true }, + ColumnsDef: [], + Constraints: [], + StrictMode: false, + }) +); expectType>( normalize("select * from table where id = 1;") diff --git a/js/tsconfig.json b/js/tsconfig.json index 9c6b170..0cfa82a 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -10,7 +10,6 @@ "esModuleInterop": true, "strict": true, "allowJs": true, - "skipLibCheck": true, - "types": ["types.d.ts", "@types/node"] + "skipLibCheck": true } } diff --git a/js/types.d.ts b/js/types.d.ts index 2067548..387f893 100644 --- a/js/types.d.ts +++ b/js/types.d.ts @@ -34,9 +34,11 @@ declare module "@tableland/sqlparser" { export type NormalizedStatement = sqlparser.NormalizedStatement; export type ValidatedTable = sqlparser.ValidatedTable; export type StatementType = sqlparser.StatementType; + export type CreateTable = sqlparser.CreateTable; } declare namespace sqlparser { + type CreateTable = import("./go-types").CreateTable; // StatementType is the type of SQL statement. export type StatementType = "read" | "write" | "create" | "acl"; @@ -86,4 +88,22 @@ declare namespace sqlparser { * @return A `Promise` that resolves to an array of strings. */ export function getUniqueTableNames(sql: string): Promise; + + export { CreateTable }; + + /** + * Internal API: Build an AST from a string containing (possibly multiple) SQL statement(s). + * @internal + * @param sql A string containing SQL statement(s). + * @return A `Promise` that resolves to an AST object. + */ + export function createStatementToObject(sql: string): Promise; + + /** + * Internal API: Build a string containing (possibly multiple) SQL statement(s) from an input AST. + * @internal + * @param ast An AST object. + * @return A `Promise` that resolves to a string containing (possibly multiple) SQL statement(s). + */ + export function createStatementFromObject(ast: CreateTable): Promise; } diff --git a/js/wasm_exec.js b/js/wasm_exec.js index 7f5cd40..06fb7a1 100644 --- a/js/wasm_exec.js +++ b/js/wasm_exec.js @@ -12,54 +12,54 @@ // This file has been modified for use by Tableland. (() => { - // Map multiple JavaScript environments to a single common API, - // preferring web standards over Node.js API. - // - // Environments considered: - // - Browsers - // - Node.js - // - Electron - // - Parcel - - if (typeof global !== "undefined") { - // global already exists - } else if (typeof window !== "undefined") { - window.global = window; - } else if (typeof self !== "undefined") { - self.global = self; - } else { - throw new Error("cannot export Go (neither global, window nor self is defined)"); - } - - const enosys = () => { - const err = new Error("not implemented"); - err.code = "ENOSYS"; - return err; - }; - - if (!global.process) { - global.process = { - getuid() { return -1; }, - getgid() { return -1; }, - geteuid() { return -1; }, - getegid() { return -1; }, - getgroups() { throw enosys(); }, - pid: -1, - ppid: -1, - umask() { throw enosys(); }, - cwd() { throw enosys(); }, - chdir() { throw enosys(); }, - } - } - - if (!global.performance) { - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; - } + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } // By removing the polyfills from this file we are more flexible in how we can use esbuild. // These three globals are expected to be polyfilled elsewhere, for the specific build. @@ -75,389 +75,383 @@ throw new Error("must polyfill util.TextDecoder for tinygo wasm"); } - // End of polyfills for common API. - - const encoder = new TextEncoder("utf-8"); - const decoder = new TextDecoder("utf-8"); - var logLine = []; - - global.Go = class { - constructor() { - this._callbackTimeouts = new Map(); - this._nextCallbackTimeoutID = 1; - - const mem = () => { - // The buffer may change when requesting more memory. - return new DataView(this._inst.exports.memory.buffer); - }; - - const setInt64 = (addr, v) => { - mem().setUint32(addr + 0, v, true); - mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); - }; - - const getInt64 = (addr) => { - const low = mem().getUint32(addr + 0, true); - const high = mem().getInt32(addr + 4, true); - return low + high * 4294967296; - }; - - const loadValue = (addr) => { - const f = mem().getFloat64(addr, true); - if (f === 0) { - return undefined; - } - if (!isNaN(f)) { - return f; - } - - const id = mem().getUint32(addr, true); - return this._values[id]; - }; - - const storeValue = (addr, v) => { - const nanHead = 0x7FF80000; - - if (typeof v === "number") { - if (isNaN(v)) { - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 0, true); - return; - } - if (v === 0) { - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 1, true); - return; - } - mem().setFloat64(addr, v, true); - return; - } - - switch (v) { - case undefined: - mem().setFloat64(addr, 0, true); - return; - case null: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 2, true); - return; - case true: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 3, true); - return; - case false: - mem().setUint32(addr + 4, nanHead, true); - mem().setUint32(addr, 4, true); - return; - } - - let id = this._ids.get(v); - if (id === undefined) { - id = this._idPool.pop(); - if (id === undefined) { - id = this._values.length; - } - this._values[id] = v; - this._goRefCounts[id] = 0; - this._ids.set(v, id); - } - this._goRefCounts[id]++; - let typeFlag = 1; - switch (typeof v) { - case "string": - typeFlag = 2; - break; - case "symbol": - typeFlag = 3; - break; - case "function": - typeFlag = 4; - break; - } - mem().setUint32(addr + 4, nanHead | typeFlag, true); - mem().setUint32(addr, id, true); - }; - - const loadSlice = (array, len, cap) => { - return new Uint8Array(this._inst.exports.memory.buffer, array, len); - }; - - const loadSliceOfValues = (array, len, cap) => { - const a = new Array(len); - for (let i = 0; i < len; i++) { - a[i] = loadValue(array + i * 8); - } - return a; - }; - - const loadString = (ptr, len) => { - return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); - }; - - const timeOrigin = Date.now() - performance.now(); - this.importObject = { - wasi_snapshot_preview1: { - // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write - fd_write: function (fd, iovs_ptr, iovs_len, nwritten_ptr) { - let nwritten = 0; - if (fd == 1) { - for (let iovs_i = 0; iovs_i < iovs_len; iovs_i++) { - let iov_ptr = iovs_ptr + iovs_i * 8; // assuming wasm32 - let ptr = mem().getUint32(iov_ptr + 0, true); - let len = mem().getUint32(iov_ptr + 4, true); - nwritten += len; - for (let i = 0; i < len; i++) { - let c = mem().getUint8(ptr + i); - if (c == 13) { // CR - // ignore - } else if (c == 10) { // LF - // write line - let line = decoder.decode(new Uint8Array(logLine)); - logLine = []; - console.log(line); - } else { - logLine.push(c); - } - } - } - } else { - console.error('invalid file descriptor:', fd); - } - mem().setUint32(nwritten_ptr, nwritten, true); - return 0; - }, - fd_close: () => 0, // dummy - fd_fdstat_get: () => 0, // dummy - fd_seek: () => 0, // dummy - "proc_exit": (code) => { - if (global.process) { - // Node.js - process.exit(code); - } else { - // Can't exit in a browser. - throw 'trying to exit with code ' + code; - } - }, - random_get: (bufPtr, bufLen) => { - crypto.getRandomValues(loadSlice(bufPtr, bufLen)); - return 0; - }, - }, - env: { - // func ticks() float64 - "runtime.ticks": () => { - return timeOrigin + performance.now(); - }, - - // func sleepTicks(timeout float64) - "runtime.sleepTicks": (timeout) => { - // Do not sleep, only reactivate scheduler after the given timeout. - setTimeout(this._inst.exports.go_scheduler, timeout); - }, - - // func finalizeRef(v ref) - "syscall/js.finalizeRef": (sp) => { - // Note: TinyGo does not support finalizers so this should never be - // called. - console.error('syscall/js.finalizeRef not implemented'); - }, - - // func stringVal(value string) ref - "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { - const s = loadString(value_ptr, value_len); - storeValue(ret_ptr, s); - }, - - // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { - let prop = loadString(p_ptr, p_len); - let value = loadValue(v_addr); - let result = Reflect.get(value, prop); - storeValue(retval, result); - }, - - // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { - const v = loadValue(v_addr); - const p = loadString(p_ptr, p_len); - const x = loadValue(x_addr); - Reflect.set(v, p, x); - }, - - // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { - const v = loadValue(v_addr); - const p = loadString(p_ptr, p_len); - Reflect.deleteProperty(v, p); - }, - - // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (ret_addr, v_addr, i) => { - storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); - }, - - // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { - Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); - }, - - // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { - const v = loadValue(v_addr); - const name = loadString(m_ptr, m_len); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - try { - const m = Reflect.get(v, name); - storeValue(ret_addr, Reflect.apply(m, v, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); - } - }, - - // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { - try { - const v = loadValue(v_addr); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - storeValue(ret_addr, Reflect.apply(v, undefined, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); - } - }, - - // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { - const v = loadValue(v_addr); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - try { - storeValue(ret_addr, Reflect.construct(v, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); - } - }, - - // func valueLength(v ref) int - "syscall/js.valueLength": (v_addr) => { - return loadValue(v_addr).length; - }, - - // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (ret_addr, v_addr) => { - const s = String(loadValue(v_addr)); - const str = encoder.encode(s); - storeValue(ret_addr, str); - setInt64(ret_addr + 8, str.length); - }, - - // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { - const str = loadValue(v_addr); - loadSlice(slice_ptr, slice_len, slice_cap).set(str); - }, - - // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (v_addr, t_addr) => { - return loadValue(v_addr) instanceof loadValue(t_addr); - }, - - // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = loadSlice(dest_addr, dest_len); - const src = loadValue(source_addr); - if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(num_bytes_copied_addr, toCopy.length); - mem().setUint8(returned_status_addr, 1); // Return "ok" status - }, - - // copyBytesToJS(dst ref, src []byte) (int, bool) - // Originally copied from upstream Go project, then modified: - // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 - "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = loadValue(dest_addr); - const src = loadSlice(source_addr, source_len); - if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(num_bytes_copied_addr, toCopy.length); - mem().setUint8(returned_status_addr, 1); // Return "ok" status - }, - } - }; - } - - async run(instance) { - this._inst = instance; - this._values = [ // JS values that Go currently has references to, indexed by reference id - NaN, - 0, - null, - true, - false, - global, - this, - ]; - this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map(); // mapping from JS values to reference ids - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - - const mem = new DataView(this._inst.exports.memory.buffer) - - while (true) { - const callbackPromise = new Promise((resolve) => { - this._resolveCallbackPromise = () => { - if (this.exited) { - throw new Error("bad callback: Go program has already exited"); - } - setTimeout(resolve, 0); // make sure it is asynchronous - }; - }); - this._inst.exports._start(); - if (this.exited) { - break; - } - await callbackPromise; - } - } - - _resume() { - if (this.exited) { - throw new Error("Go program has already exited"); - } - this._inst.exports.resume(); - if (this.exited) { - this._resolveExitPromise(); - } - } - - _makeFuncWrapper(id) { - const go = this; - return function () { - const event = { id: id, this: this, args: arguments }; - go._pendingEvent = event; - go._resume(); - return event.result; - }; - } - } + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + let reinterpretBuf = new DataView(new ArrayBuffer(8)); + var logLine = []; + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true); + const f = reinterpretBuf.getFloat64(0, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = v_ref & 0xffffffffn; + return this._values[id]; + } + + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true); + return unboxValue(v_ref); + } + + const boxValue = (v) => { + const nanHead = 0x7FF80000n; + + if (typeof v === "number") { + if (isNaN(v)) { + return nanHead << 32n; + } + if (v === 0) { + return (nanHead << 32n) | 1n; + } + reinterpretBuf.setFloat64(0, v, true); + return reinterpretBuf.getBigInt64(0, true); + } + + switch (v) { + case undefined: + return 0n; + case null: + return (nanHead << 32n) | 2n; + case true: + return (nanHead << 32n) | 3n; + case false: + return (nanHead << 32n) | 4n; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = BigInt(this._values.length); + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1n; + switch (typeof v) { + case "string": + typeFlag = 2n; + break; + case "symbol": + typeFlag = 3n; + break; + case "function": + typeFlag = 4n; + break; + } + return id | ((nanHead | typeFlag) << 32n); + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v); + mem().setBigUint64(addr, v_ref, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + "proc_exit": (code) => { + if (global.process) { + // Node.js + process.exit(code); + } else { + // Can't exit in a browser. + throw 'trying to exit with code ' + code; + } + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + gojs: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(this._inst.exports.go_scheduler, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (v_ref) => { + // Note: TinyGo does not support finalizers so this should never be + // called. + console.error('syscall/js.finalizeRef not implemented'); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (value_ptr, value_len) => { + const s = loadString(value_ptr, value_len); + return boxValue(s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let v = unboxValue(v_ref); + let result = Reflect.get(v, prop); + return boxValue(result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + const x = unboxValue(x_ref); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + try { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_ref) => { + return unboxValue(v_ref).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + mem().setInt32(ret_addr + 8, str.length, true); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { + const str = unboxValue(v_ref); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = unboxValue(src_ref); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = unboxValue(dst_ref); + const src = loadSlice(src_addr, src_len); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // For compatibility, we use both as long as Go 1.20 is supported. + this.importObject.env = this.importObject.gojs; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + const mem = new DataView(this._inst.exports.memory.buffer) + + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = () => { + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(resolve, 0); // make sure it is asynchronous + }; + }); + this._inst.exports._start(); + if (this.exited) { + break; + } + await callbackPromise; + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } })();