Skip to content

Commit

Permalink
Merge pull request #307 from ag5/feature/daterange
Browse files Browse the repository at this point in the history
Feature/daterange
  • Loading branch information
go-jet committed Feb 26, 2024
2 parents a2eb15e + 893567d commit f9f8268
Show file tree
Hide file tree
Showing 61 changed files with 2,988 additions and 215 deletions.
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
build_and_tests:
docker:
# specify the version
- image: circleci/golang:1.16
- image: circleci/postgres:12
- image: cimg/go:1.21.6
- image: cimg/postgres:14.10
environment:
POSTGRES_USER: jet
POSTGRES_PASSWORD: jet
Expand All @@ -33,8 +33,8 @@ jobs:
MYSQL_USER: jet
MYSQL_PASSWORD: jet

- image: cockroachdb/cockroach-unstable:v22.1.0-beta.4
command: ['start-single-node', '--insecure']
- image: cockroachdb/cockroach-unstable:v23.1.0-rc.2
command: ['start-single-node', '--accept-sql-without-tls']
environment:
COCKROACH_USER: jet
COCKROACH_PASSWORD: jet
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,33 +105,33 @@ Done
```
Procedure is similar for MySQL, CockroachDB, MariaDB and SQLite. For example:
```sh
jet -source=mysql -dsn="user:pass@tcp(localhost:3306)/dbname" -path=./gen
jet -source=mysql -dsn="user:pass@tcp(localhost:3306)/dbname" -path=./.gen
jet -dsn=postgres://user:pass@localhost:26257/jetdb?sslmode=disable -schema=dvds -path=./.gen #cockroachdb
jet -dsn="mariadb://user:pass@tcp(localhost:3306)/dvds" -path=./gen # source flag can be omitted if data source appears in dsn
jet -source=sqlite -dsn="/path/to/sqlite/database/file" -schema=dvds -path=./gen
jet -dsn="file:///path/to/sqlite/database/file" -schema=dvds -path=./gen # sqlite database assumed for 'file' data sources
jet -dsn="mariadb://user:pass@tcp(localhost:3306)/dvds" -path=./.gen # source flag can be omitted if data source appears in dsn
jet -source=sqlite -dsn="/path/to/sqlite/database/file" -schema=dvds -path=./.gen
jet -dsn="file:///path/to/sqlite/database/file" -schema=dvds -path=./.gen # sqlite database assumed for 'file' data sources
```
_*User has to have a permission to read information schema tables._

As command output suggest, Jet will:
- connect to postgres database and retrieve information about the _tables_, _views_ and _enums_ of `dvds` schema
- delete everything in schema destination folder - `./gen/jetdb/dvds`,
- delete everything in schema destination folder - `./.gen/jetdb/dvds`,
- and finally generate SQL Builder and Model types for each schema table, view and enum.


Generated files folder structure will look like this:
```sh
|-- gen # -path
| `-- jetdb # database name
| `-- dvds # schema name
|-- .gen # path
| -- jetdb # database name
| -- dvds # schema name
| |-- enum # sql builder package for enums
| | |-- mpaa_rating.go
| |-- table # sql builder package for tables
| |-- actor.go
| |-- address.go
| |-- category.go
| ...
| |-- view # sql builder package for views
| |-- view # sql builder package for views
| |-- actor_info.go
| |-- film_list.go
| ...
Expand All @@ -156,7 +156,7 @@ import (
. "github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/table"
. "github.com/go-jet/jet/v2/postgres"

"github.com/go-jet/jet/v2/examples/quick-start/gen/jetdb/dvds/model"
"github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/model"
)
```
Let's say we want to retrieve the list of all _actors_ that acted in _films_ longer than 180 minutes, _film language_ is 'English'
Expand Down Expand Up @@ -530,8 +530,8 @@ Automatic scan to arbitrary structure removes a lot of headache and boilerplate

##### Speed of execution

While ORM libraries can introduce significant performance penalties due to number of round-trips to the database,
Jet will always perform better as developers can write complex query and retrieve result with a single database call.
While ORM libraries can introduce significant performance penalties due to number of round-trips to the database(N+1 query problem),
`jet` will always perform better as developers can write complex query and retrieve result with a single database call.
Thus handler time lost on latency between server and database can be constant. Handler execution will be proportional
only to the query complexity and the number of rows returned from database.

Expand Down Expand Up @@ -579,5 +579,5 @@ To run the tests, additional dependencies are required:

## License

Copyright 2019-2023 Goran Bjelanovic
Copyright 2019-2024 Goran Bjelanovic
Licensed under the Apache License, Version 2.0.
3 changes: 2 additions & 1 deletion generator/metadata/column_meta_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

// Column struct
type Column struct {
Name string
Name string `sql:"primary_key"`
IsPrimaryKey bool
IsNullable bool
IsGenerated bool
Expand All @@ -33,6 +33,7 @@ const (
EnumType DataTypeKind = "enum"
UserDefinedType DataTypeKind = "user-defined"
ArrayType DataTypeKind = "array"
RangeType DataTypeKind = "range"
)

// DataType contains information about column data type
Expand Down
2 changes: 1 addition & 1 deletion generator/metadata/table_meta_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package metadata

// Table metadata struct
type Table struct {
Name string
Name string `sql:"primary_key"`
Columns []Column
}

Expand Down
5 changes: 5 additions & 0 deletions generator/mysql/mysql_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
mysqldr "github.com/go-sql-driver/mysql"
)

const mysqlMaxConns = 10

// DBConnection contains MySQL connection details
type DBConnection struct {
Host string
Expand Down Expand Up @@ -83,6 +85,9 @@ func openConnection(connectionString string) (*sql.DB, error) {
return nil, fmt.Errorf("failed to open mysql connection: %w", err)
}

db.SetMaxOpenConns(mysqlMaxConns)
db.SetMaxIdleConns(mysqlMaxConns)

err = db.Ping()
if err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
Expand Down
80 changes: 34 additions & 46 deletions generator/mysql/query_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,60 +15,48 @@ type mySqlQuerySet struct{}

func (m mySqlQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
query := `
SELECT table_name as "table.name"
FROM INFORMATION_SCHEMA.tables
WHERE table_schema = ? and table_type = ?
ORDER BY table_name;
`
SELECT
t.table_name as "table.name",
col.COLUMN_NAME AS "column.Name",
col.IS_NULLABLE = "YES" AS "column.IsNullable",
col.COLUMN_COMMENT AS "column.Comment",
COALESCE(pk.IsPrimaryKey, 0) AS "column.IsPrimaryKey",
IF (col.COLUMN_TYPE = 'tinyint(1)',
'boolean',
IF (col.DATA_TYPE = 'enum',
CONCAT(col.TABLE_NAME, '_', col.COLUMN_NAME),
col.DATA_TYPE)
) AS "dataType.Name",
IF (col.DATA_TYPE = 'enum', 'enum', 'base') AS "dataType.Kind",
col.COLUMN_TYPE LIKE '%unsigned%' AS "dataType.IsUnsigned"
FROM INFORMATION_SCHEMA.tables AS t
INNER JOIN
information_schema.columns AS col
ON t.table_schema = col.table_schema AND t.table_name = col.table_name
LEFT JOIN (
SELECT k.column_name, 1 AS IsPrimaryKey, k.table_name
FROM information_schema.table_constraints t
JOIN information_schema.key_column_usage k USING(constraint_name, table_schema, table_name)
WHERE t.table_schema = ?
AND t.constraint_type = 'PRIMARY KEY'
) AS pk ON col.COLUMN_NAME = pk.column_name AND col.table_name = pk.table_name
WHERE t.table_schema = ?
AND t.table_type = ?
ORDER BY
t.table_name,
col.ordinal_position;
`

var tables []metadata.Table

_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableType}, &tables)
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, schemaName, tableType}, &tables)
if err != nil {
return nil, fmt.Errorf("failed to query %s metadata result: %w", tableType, err)
}

for i := range tables {
tables[i].Columns, err = m.GetTableColumnsMetaData(db, schemaName, tables[i].Name)
if err != nil {
return nil, fmt.Errorf("failed to get '%s' table columns metadata: %w", tables[i].Name, err)
}
return nil, fmt.Errorf("failed to query column meta data: %w", err)
}

return tables, nil
}

func (m mySqlQuerySet) GetTableColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
query := `
SELECT COLUMN_NAME AS "column.Name",
IS_NULLABLE = "YES" AS "column.IsNullable",
columns.COLUMN_COMMENT as "column.Comment",
(EXISTS(
SELECT 1
FROM information_schema.table_constraints t
JOIN information_schema.key_column_usage k USING(constraint_name,table_schema,table_name)
WHERE table_schema = ? AND table_name = ? AND t.constraint_type='PRIMARY KEY' AND k.column_name = columns.column_name
)) AS "column.IsPrimaryKey",
IF (COLUMN_TYPE = 'tinyint(1)',
'boolean',
IF (DATA_TYPE='enum',
CONCAT(TABLE_NAME, '_', COLUMN_NAME),
DATA_TYPE)
) AS "dataType.Name",
IF (DATA_TYPE = 'enum', 'enum', 'base') AS "dataType.Kind",
COLUMN_TYPE LIKE '%unsigned%' AS "dataType.IsUnsigned"
FROM information_schema.columns
WHERE table_schema = ? AND table_name = ?
ORDER BY ordinal_position;
`
var columns []metadata.Column
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableName, schemaName, tableName}, &columns)
if err != nil {
return nil, fmt.Errorf("failed to query %s column meta data: %w", tableName, err)
}

return columns, nil
}

func (m mySqlQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
query := `
SELECT (CASE c.DATA_TYPE WHEN 'enum' then CONCAT(c.TABLE_NAME, '_', c.COLUMN_NAME) ELSE '' END ) as "name",
Expand Down
83 changes: 50 additions & 33 deletions generator/postgres/query_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,25 @@ ORDER BY table_name;
return nil, fmt.Errorf("failed to query %s metadata: %w", tableType, err)
}

// add materialized views separately, because materialized views are not part of standard information schema
if tableType == metadata.ViewTable {
matViewQuery := `
select matviewname as "table.name"
from pg_matviews
where schemaname = $1;
`
var matViews []metadata.Table

_, err := qrm.Query(context.Background(), db, matViewQuery, []interface{}{schemaName}, &matViews)
if err != nil {
return nil, fmt.Errorf("failed to query materialized view metadata: %w", err)
}

tables = append(tables, matViews...)
}

for i := range tables {
tables[i].Columns, err = p.GetTableColumnsMetaData(db, schemaName, tables[i].Name)
tables[i].Columns, err = getColumnsMetaData(db, schemaName, tables[i].Name)
if err != nil {
return nil, fmt.Errorf("failed to query %s columns metadata: %w", tableType, err)
}
Expand All @@ -36,39 +53,39 @@ ORDER BY table_name;
return tables, nil
}

func (p postgresQuerySet) GetTableColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
func getColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
query := `
WITH primaryKeys AS (
SELECT column_name
FROM information_schema.key_column_usage AS c
LEFT JOIN information_schema.table_constraints AS t
ON t.constraint_name = c.constraint_name AND
c.table_schema = t.table_schema AND
c.table_name = t.table_name
WHERE t.table_schema = $1 AND t.table_name = $2 AND t.constraint_type = 'PRIMARY KEY'
)
SELECT column_name as "column.Name",
is_nullable = 'YES' as "column.isNullable",
is_generated = 'ALWAYS' or is_generated = 'YES' as "column.isGenerated",
(EXISTS(SELECT 1 from primaryKeys as pk where pk.column_name = columns.column_name)) as "column.IsPrimaryKey",
dataType.kind as "dataType.Kind",
(case dataType.Kind when 'base' then data_type else LTRIM(udt_name, '_') end) as "dataType.Name",
FALSE as "dataType.isUnsigned"
FROM information_schema.columns,
LATERAL (select (case data_type
when 'ARRAY' then 'array'
when 'USER-DEFINED' then
case (select t.typtype
from pg_type as t
join pg_namespace as p on p.oid = t.typnamespace
where t.typname = columns.udt_name and p.nspname = $1)
when 'e' then 'enum'
else 'user-defined'
end
else 'base'
end) as Kind) as dataType
where table_schema = $1 and table_name = $2
order by ordinal_position;
select
attr.attname as "column.Name",
exists(
select 1
from pg_catalog.pg_index indx
where attr.attrelid = indx.indrelid and attr.attnum = any(indx.indkey) and indx.indisprimary
) as "column.IsPrimaryKey",
not attr.attnotnull as "column.isNullable",
attr.attgenerated = 's' as "column.isGenerated",
(case tp.typtype
when 'b' then 'base'
when 'd' then 'base'
when 'e' then 'enum'
when 'r' then 'range'
end) as "dataType.Kind",
(case when tp.typtype = 'd' then (select pg_type.typname from pg_catalog.pg_type where pg_type.oid = tp.typbasetype)
when tp.typcategory = 'A' then pg_catalog.format_type(attr.atttypid, attr.atttypmod)
else tp.typname
end) as "dataType.Name",
false as "dataType.isUnsigned"
from pg_catalog.pg_attribute as attr
join pg_catalog.pg_class as cls on cls.oid = attr.attrelid
join pg_catalog.pg_namespace as ns on ns.oid = cls.relnamespace
join pg_catalog.pg_type as tp on tp.oid = attr.atttypid
where
ns.nspname = $1 and
cls.relname = $2 and
not attr.attisdropped and
attr.attnum > 0
order by
attr.attnum;
`
var columns []metadata.Column
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableName}, &columns)
Expand Down
Loading

0 comments on commit f9f8268

Please sign in to comment.