diff --git a/tests/select_test.go b/tests/select_test.go index 123d988..ce8a066 100644 --- a/tests/select_test.go +++ b/tests/select_test.go @@ -314,6 +314,179 @@ LIMIT 15; assert.Equal(t, len(filmsPerLanguage[0].Film), limit) } +func TestExecution1(t *testing.T) { + stmt := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID, + City.City, + Address.AddressID, + Address.Address, + Customer.CustomerID, + Customer.LastName, + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + + assertStatementSql(t, stmt, ` +SELECT city.city_id AS "city.city_id", + city.city AS "city.city", + address.address_id AS "address.address_id", + address.address AS "address.address", + customer.customer_id AS "customer.customer_id", + customer.last_name AS "customer.last_name" +FROM dvds.city + INNER JOIN dvds.address ON (address.city_id = city.city_id) + INNER JOIN dvds.customer ON (customer.address_id = address.address_id) +WHERE (city.city = 'London') OR (city.city = 'York') +ORDER BY city.city_id, address.address_id, customer.customer_id; +`, "London", "York") + + var dest []struct { + model.City + + Customers []struct { + model.Customer + + Address model.Address + } + } + + err := stmt.Query(db, &dest) + + assert.NilError(t, err) + + assert.Equal(t, len(dest), 2) + assert.Equal(t, dest[0].City.City, "London") + assert.Equal(t, dest[1].City.City, "York") + assert.Equal(t, len(dest[0].Customers), 2) + assert.Equal(t, dest[0].Customers[0].LastName, "Hoffman") + assert.Equal(t, dest[0].Customers[1].LastName, "Vines") + +} + +func TestExecution2(t *testing.T) { + + type MyAddress struct { + ID int32 `sql:"primary_key"` + AddressLine string + } + + type MyCustomer struct { + ID int32 `sql:"primary_key"` + LastName *string + + Address MyAddress + } + + type MyCity struct { + ID int32 `sql:"primary_key"` + Name string + + Customers []MyCustomer + } + + dest := []MyCity{} + + stmt := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID.AS("my_city.id"), + City.City.AS("myCity.Name"), + Address.AddressID.AS("My_Address.id"), + Address.Address.AS("my address.address line"), + Customer.CustomerID.AS("my_customer.id"), + Customer.LastName.AS("my_customer.last_name"), + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + + assertStatementSql(t, stmt, ` +SELECT city.city_id AS "my_city.id", + city.city AS "myCity.Name", + address.address_id AS "My_Address.id", + address.address AS "my address.address line", + customer.customer_id AS "my_customer.id", + customer.last_name AS "my_customer.last_name" +FROM dvds.city + INNER JOIN dvds.address ON (address.city_id = city.city_id) + INNER JOIN dvds.customer ON (customer.address_id = address.address_id) +WHERE (city.city = 'London') OR (city.city = 'York') +ORDER BY city.city_id, address.address_id, customer.customer_id; +`, "London", "York") + + err := stmt.Query(db, &dest) + + assert.NilError(t, err) + + assert.Equal(t, len(dest), 2) + assert.Equal(t, dest[0].Name, "London") + assert.Equal(t, dest[1].Name, "York") + assert.Equal(t, len(dest[0].Customers), 2) + assert.Equal(t, *dest[0].Customers[0].LastName, "Hoffman") + assert.Equal(t, *dest[0].Customers[1].LastName, "Vines") + +} + +func TestExecution3(t *testing.T) { + + var dest []struct { + CityID int32 `sql:"primary_key"` + CityName string + + Customers []struct { + CustomerID int32 `sql:"primary_key"` + LastName *string + + Address struct { + AddressID int32 `sql:"primary_key"` + AddressLine string + } + } + } + + stmt := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID.AS("city_id"), + City.City.AS("city_name"), + Customer.CustomerID.AS("customer_id"), + Customer.LastName.AS("last_name"), + Address.AddressID.AS("address_id"), + Address.Address.AS("address_line"), + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + + assertStatementSql(t, stmt, ` +SELECT city.city_id AS "city_id", + city.city AS "city_name", + customer.customer_id AS "customer_id", + customer.last_name AS "last_name", + address.address_id AS "address_id", + address.address AS "address_line" +FROM dvds.city + INNER JOIN dvds.address ON (address.city_id = city.city_id) + INNER JOIN dvds.customer ON (customer.address_id = address.address_id) +WHERE (city.city = 'London') OR (city.city = 'York') +ORDER BY city.city_id, address.address_id, customer.customer_id; +`, "London", "York") + + err := stmt.Query(db, &dest) + + assert.NilError(t, err) + + assert.Equal(t, len(dest), 2) + assert.Equal(t, dest[0].CityName, "London") + assert.Equal(t, dest[1].CityName, "York") + assert.Equal(t, len(dest[0].Customers), 2) + assert.Equal(t, *dest[0].Customers[0].LastName, "Hoffman") + assert.Equal(t, *dest[0].Customers[1].LastName, "Vines") +} + func TestJoinQuerySliceWithPtrs(t *testing.T) { type FilmsPerLanguage struct { Language model.Language diff --git a/wiki/SQL-Builder.md b/wiki/SQL-Builder.md index b3d40db..efb2ed1 100644 --- a/wiki/SQL-Builder.md +++ b/wiki/SQL-Builder.md @@ -2,16 +2,16 @@ ## SQL Builder SQL Builder files are Go files, containing types necessary to write type safe SQL queries in Go. They are -autogenerated from database tables and enums. +autogenerated from database tables and enums. File names are snake case of the table name or enum name. ### Table SQL Builder files Following rules are applied to generate table SQL Builder files: -- for every table there is one Go SQL Builder file generated. File name is in snake case of the table name. -- every file contains one type - struct with nested jet.Table. Type name is a camel case of table name. -- for every column of table there is a field column in SQL Builder table type. Field name is camel case of column name. -See below table for type mapping. +- for every table there is one Go SQL Builder file generated. +- every file contains one type - struct with nested jet.Table. +- for every column of table there is a field column in SQL Builder table type. +Field name is camel case of column name. See below table for type mapping. - `AllColumns` is used as shorthand notation for list of all columns. - `MutableColumns` are all columns minus primary key columns _(Useful in INSERT or UPDATE statements)_. @@ -82,7 +82,7 @@ type AddressTable struct { Following rules are applied to generate enum SQL Builder files: -- for every enum there is one Go SQL Builder file generated. File name is in snake case of the enum name. +- for every enum there is one Go SQL Builder file generated. - every file contains one type. Type name is a camel case of enum name. - for every enum value there is a field in SQL Builder enum struct. Field name is camel case of enum value. Type is jet.StringExpression, meaning it can be used by string expressions methods. diff --git a/wiki/Scan-to-arbitrary-destination.md b/wiki/Scan-to-arbitrary-destination.md new file mode 100644 index 0000000..038c106 --- /dev/null +++ b/wiki/Scan-to-arbitrary-destination.md @@ -0,0 +1,349 @@ +## Scan to arbitrary destination + +Statements `Query` and `QueryContext` methods perform scan and grouping of row result to arbitrary `destination` structure. + +- `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination. +- `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination. + + +### How scan works? + +The easiest way to understand how scan works is by an example. + +Lets say we want to retrieve list of cities, with list of customers for each city, and address for each customer. +For simplicity we will narrow the choice to 'London' and 'York'. + +Go SQL builder select statement: +``` +stmt := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID, + City.City, + Address.AddressID, + Address.Address, + Customer.CustomerID, + Customer.LastName, + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) +``` +_Note that we are using jet select statement format([TODO]())_ + +Debug sql of above statement: +``` +SELECT city.city_id AS "city.city_id", + city.city AS "city.city", + address.address_id AS "address.address_id", + address.address AS "address.address", + customer.customer_id AS "customer.customer_id", + customer.last_name AS "customer.last_name" +FROM dvds.city + INNER JOIN dvds.address ON (address.city_id = city.city_id) + INNER JOIN dvds.customer ON (customer.address_id = address.address_id) +WHERE (city.city = 'London') OR (city.city = 'York') +ORDER BY city.city_id, address.address_id, customer.customer_id; +``` + +Every column is aliased by default. Format is "`table_name`.`column_name`" + +Above statement will produce following result set: + +|_row_| city.city_id | city.city | address.address_id | address.address | customer.customer_id | customer.last_name | +|---| ------------ | ------------- | ------------------- | -------------------- | -------------------- | ------------------ | +| _1_| 312 | "London" | 256 | "1497 Yuzhou Drive" | 252 | "Hoffman"| +| _2_| 312 | "London" | 517 | "548 Uruapan Street"| 512 | "Vines" | +| _3_| 589 | "York" | 502 | "1515 Korla Way" | 497 | "Sledge" | + +Lets execute statement and scan result set to destination `dest`: + ``` +var dest []struct { + model.City + + Customers []struct{ + model.Customer + + Address model.Address + } +} + +err := stmt.Query(db, &dest) + ``` + +`Query` uses reflection to introspect destination type structure, and result set column names(aliases), to be able to map result set data to destination object. +Note that camel case of result set column names(aliases) is the same as `model type name`.`field name`. +For instance `city.city_id` -> `City.CityID`. This is being used to find appropriate column for each destination model field. +It is not an error if there is not a column for each destination model field. + +Lets see in general how `Query` works row by row: + +- ROW 1: + - dest is slice of structs, so new struct object is initialized and scan proceeds to next step. + - `city.city_id` and `city.city` columns (values `312` and `"London"`) are used to initialize `CityID` and `City` fields of `model.City` object. + - `Customers` is a slice of structs, so new struct object is initialized and scan proceeds to next step. + - `customer.customer_id` and `customer.last_name` is used to initialize fields in `model.Customer` object. + - `address.address_id` and `address.address` is used to initialize fields in `Address model.Address` + - because at least one field of struct is being initialized struct is added to `Customers []struct` and cached by parent and + struct primary key fields([more about primary key fields](TODO)). Primary keys used for caching are `CityID`, `CustomerID` and `AddressID` of `model.City`, `model.Customer` + and `model.Address` + - because at least one field of struct is being initialized struct is added to `var dest []struct` and cached by + struct primary key fields. Primary keys used for caching is only `CityID` from `model.City` +- ROW 2: + - Does not initialize new struct object for `dest []struct` but pulls one from the cache, because `city` with `city_id` of `312` has + already being processed. Following steps are the same as above, new objects are created, stored in slice and cached. +- ROW 3: + - steps would be similar as for the first step. Nothing is pulled from he cache, stored in slice and cached. + + + Lets print `dest` as a json, to visualize `Query` result: + + ``` + [ + { + "CityID": 312, + "City": "London", + "CountryID": 0, + "LastUpdate": "0001-01-01T00:00:00Z", + "Customers": [ + { + "CustomerID": 252, + "StoreID": 0, + "FirstName": "", + "LastName": "Hoffman", + "Email": null, + "AddressID": 0, + "Activebool": false, + "CreateDate": "0001-01-01T00:00:00Z", + "LastUpdate": null, + "Active": null, + "Address": { + "AddressID": 256, + "Address": "1497 Yuzhou Drive", + "Address2": null, + "District": "", + "CityID": 0, + "PostalCode": null, + "Phone": "", + "LastUpdate": "0001-01-01T00:00:00Z" + } + }, + { + "CustomerID": 512, + "StoreID": 0, + "FirstName": "", + "LastName": "Vines", + "Email": null, + "AddressID": 0, + "Activebool": false, + "CreateDate": "0001-01-01T00:00:00Z", + "LastUpdate": null, + "Active": null, + "Address": { + "AddressID": 517, + "Address": "548 Uruapan Street", + "Address2": null, + "District": "", + "CityID": 0, + "PostalCode": null, + "Phone": "", + "LastUpdate": "0001-01-01T00:00:00Z" + } + } + ] + }, + { + "CityID": 589, + "City": "York", + "CountryID": 0, + "LastUpdate": "0001-01-01T00:00:00Z", + "Customers": [ + { + "CustomerID": 497, + "StoreID": 0, + "FirstName": "", + "LastName": "Sledge", + "Email": null, + "AddressID": 0, + "Activebool": false, + "CreateDate": "0001-01-01T00:00:00Z", + "LastUpdate": null, + "Active": null, + "Address": { + "AddressID": 502, + "Address": "1515 Korla Way", + "Address2": null, + "District": "", + "CityID": 0, + "PostalCode": null, + "Phone": "", + "LastUpdate": "0001-01-01T00:00:00Z" + } + } + ] + } + ] + ``` + +All the fields missing source column in result set are initialized with empty value. +City of `London` has two customers, which is the product of object reuse in `ROW 2` processing. + +### Custom model files + +**Programmes are not limited to just model files, any destination will work, as long as camel case of result set column +is equal to `model type name`.`field name`.** + +#### Named types + +Lets rewrite above example to use custom named model files: + +``` +type MyAddress struct { + ID int32 `sql:"primary_key"` + AddressLine string +} + +type MyCustomer struct { + ID int32 `sql:"primary_key"` + LastName *string + + Address MyAddress +} + +type MyCity struct { + ID int32 `sql:"primary_key"` + Name string + + Customers []MyCustomer +} + +dest2 := []MyCity{} + +stmt2 := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID.AS("my_city.id"), //snake case + City.City.AS("myCity.Name"), //camel case + Address.AddressID.AS("My_Address.id"), //mixed case + Address.Address.AS("my address.address line"), //with spaces + Customer.CustomerID.AS("my_customer.id"), + Customer.LastName.AS("my_customer.last_name"), + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + +err := stmt2.Query(db, &dest2) +``` + +Destination type names and field names are now changed. Every type has 'My' prefix, every primary key column is named `ID`, + `FirstName` is now string pointer etc. +Because we are using custom types with changed identifier, every column has to be aliased. +For instance: `City.CityID.AS("my_city.id")`, ` City.City.AS("myCity.Name")` etc. +**Table names, column names and aliases doesn't have to be in a snake case. CamelCase, PascalCase or some other mixed space is also supported, +but it is strongly recommended to use snake case for database identifiers.** + +Json of new destination is also changed: + +``` +[ + { + "ID": 312, + "Name": "London", + "Customers": [ + { + "ID": 252, + "LastName": "Hoffman", + "Address": { + "ID": 256, + "AddressLine": "1497 Yuzhou Drive" + } + }, + { + "ID": 512, + "LastName": "Vines", + "Address": { + "ID": 517, + "AddressLine": "548 Uruapan Street" + } + } + ] + }, + { + "ID": 589, + "Name": "York", + "Customers": [ + { + "ID": 497, + "LastName": "Sledge", + "Address": { + "ID": 502, + "AddressLine": "1515 Korla Way" + } + } + ] + } +] +``` + +#### Antonymous types + +There is no need to create new named type for every custom model. +Destination type can be declared inline without naming any type. + +``` +var dest []struct { + CityID int32 `sql:"primary_key"` + CityName string + + Customers []struct { + CustomerID int32 `sql:"primary_key"` + LastName string + + Address struct { + AddressID int32 `sql:"primary_key"` + AddressLine string + } + } +} + +stmt := City. + INNER_JOIN(Address, Address.CityID.EQ(City.CityID)). + INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)). + SELECT( + City.CityID.AS("city_id"), + City.City.AS("city_name"), + Customer.CustomerID.AS("customer_id"), + Customer.LastName.AS("last_name"), + Address.AddressID.AS("address_id"), + Address.Address.AS("address_line"), + ). + WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). + ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) + +err := stmt.Query(db, &dest) +``` +Aliasing is now simplified. Alias contains only (column/field) name. +On the other hand, we can not have 3 fields named `ID`, because aliases have to be unique. + + +### Combining autogenerated and custom model files + +It is allowed to combine autogenerated and custom model files. +For instance: + +``` +type MyCustomer struct { + ID int32 `sql:"primary_key"` + LastName string + + Address model.Address //model.Address is autogenerated model type +} + +type MyCity struct { + ID int32 `sql:"primary_key"` + Name string + + Customers []MyCustomer +} +``` \ No newline at end of file diff --git a/wiki/Statements.md b/wiki/Statements.md index dfae709..c5e769e 100644 --- a/wiki/Statements.md +++ b/wiki/Statements.md @@ -9,15 +9,23 @@ Following statements are supported: _This list might be extended with feature Jet releases._ -There is a common set of action that can be performed for each statement type: +Statements SQL can be debugged in two ways: - `Sql() (query string, args []interface{}, err error)` - retrieves parametrized sql query with list of arguments - `DebugSql() (query string, err error)` - retrieves debug query where every parametrized placeholder is replaced with its argument. + +Statements can be executed by following methods: - `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination. - `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination. - `Exec(db execution.DB) (sql.Result, error)` - executes statement over db connection without returning any rows. - `ExecContext(db execution.DB, context context.Context) (sql.Result, error)` - executes statement with context over db connection without returning any rows. +Each execution method first creates parametrized sql query with list of arguments and then initiates query on database connection. +Exec and ExecContext are just a wrappers around database `Exec` and `ExecContext`. + +`Query` and `QueryContext` are bit more complex, the are wrappers around database `Query` and `QueryContext`, +but they also perform grouping of row result to arbitrary `destination` structure. + Database connection can be of any type that implements following interface: ```go diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 169e2d7..e504309 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -10,4 +10,4 @@ * [UPDATE](https://github.com/go-jet/jet/wiki/UPDATE) * [DELETE](https://github.com/go-jet/jet/wiki/DELETE) * [LOCK](https://github.com/go-jet/jet/wiki/LOCK) -* [Execution](https://github.com/go-jet/jet/wiki/Execution) \ No newline at end of file +* [Scan to arbitrary destination](https://github.com/go-jet/jet/wiki/Scan-to-arbitrary-destination.md)