From f71d33c623abbe617dd6f12753b3e9af17ac85f6 Mon Sep 17 00:00:00 2001 From: Dmitrii Levin Date: Wed, 6 Mar 2024 13:08:30 +0200 Subject: [PATCH] Add partition to table type, update createdb template --- .../postgres/apartitiontable.xo.go | 94 +++++++++++++++++++ .../a_bit_of_everything/postgres/xo.xo.dot | 7 ++ .../a_bit_of_everything/postgres/xo.xo.json | 63 +++++++++++++ .../a_bit_of_everything/postgres/xo.xo.sql | 8 ++ .../a_bit_of_everything/postgres/xo.xo.yaml | 36 +++++++ .../sql/postgres_schema.sql | 7 ++ cmd/schema.go | 10 +- gen.sh | 8 +- models/table.xo.go | 12 ++- templates/createdb/xo.xo.sql.tpl | 16 +++- types/types.go | 7 ++ 11 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 _examples/a_bit_of_everything/postgres/apartitiontable.xo.go diff --git a/_examples/a_bit_of_everything/postgres/apartitiontable.xo.go b/_examples/a_bit_of_everything/postgres/apartitiontable.xo.go new file mode 100644 index 00000000..46377457 --- /dev/null +++ b/_examples/a_bit_of_everything/postgres/apartitiontable.xo.go @@ -0,0 +1,94 @@ +package postgres + +// Code generated by xo. DO NOT EDIT. + +import ( + "context" + "time" +) + +// APartitionTable represents a row from 'public.a_partition_table'. +type APartitionTable struct { + AKey1 int `json:"a_key1"` // a_key1 + AKey2 time.Time `json:"a_key2"` // a_key2 + // xo fields + _exists, _deleted bool +} + +// Exists returns true when the [APartitionTable] exists in the database. +func (apt *APartitionTable) Exists() bool { + return apt._exists +} + +// Deleted returns true when the [APartitionTable] has been marked for deletion +// from the database. +func (apt *APartitionTable) Deleted() bool { + return apt._deleted +} + +// Insert inserts the [APartitionTable] to the database. +func (apt *APartitionTable) Insert(ctx context.Context, db DB) error { + switch { + case apt._exists: // already exists + return logerror(&ErrInsertFailed{ErrAlreadyExists}) + case apt._deleted: // deleted + return logerror(&ErrInsertFailed{ErrMarkedForDeletion}) + } + // insert (manual) + const sqlstr = `INSERT INTO public.a_partition_table (` + + `a_key1, a_key2` + + `) VALUES (` + + `$1, $2` + + `)` + // run + logf(sqlstr, apt.AKey1, apt.AKey2) + if _, err := db.ExecContext(ctx, sqlstr, apt.AKey1, apt.AKey2); err != nil { + return logerror(err) + } + // set exists + apt._exists = true + return nil +} + +// ------ NOTE: Update statements omitted due to lack of fields other than primary key ------ + +// Delete deletes the [APartitionTable] from the database. +func (apt *APartitionTable) Delete(ctx context.Context, db DB) error { + switch { + case !apt._exists: // doesn't exist + return nil + case apt._deleted: // deleted + return nil + } + // delete with composite primary key + const sqlstr = `DELETE FROM public.a_partition_table ` + + `WHERE a_key1 = $1 AND a_key2 = $2` + // run + logf(sqlstr, apt.AKey1, apt.AKey2) + if _, err := db.ExecContext(ctx, sqlstr, apt.AKey1, apt.AKey2); err != nil { + return logerror(err) + } + // set deleted + apt._deleted = true + return nil +} + +// APartitionTableByAKey1AKey2 retrieves a row from 'public.a_partition_table' as a [APartitionTable]. +// +// Generated from index 'a_partition_table_pkey'. +func APartitionTableByAKey1AKey2(ctx context.Context, db DB, aKey1 int, aKey2 time.Time) (*APartitionTable, error) { + // query + const sqlstr = `SELECT ` + + `a_key1, a_key2 ` + + `FROM public.a_partition_table ` + + `WHERE a_key1 = $1 AND a_key2 = $2` + // run + logf(sqlstr, aKey1, aKey2) + apt := APartitionTable{ + _exists: true, + } + if err := db.QueryRowContext(ctx, sqlstr, aKey1, aKey2).Scan(&apt.AKey1, &apt.AKey2); err != nil { + return nil, logerror(err) + } + return &apt, nil +} diff --git a/_examples/a_bit_of_everything/postgres/xo.xo.dot b/_examples/a_bit_of_everything/postgres/xo.xo.dot index 7dd4daaf..02f433f4 100644 --- a/_examples/a_bit_of_everything/postgres/xo.xo.dot +++ b/_examples/a_bit_of_everything/postgres/xo.xo.dot @@ -107,6 +107,13 @@ digraph public { "public.a_manual_table" a_text: character varying > ] + + "public.a_partition_table" [ label=< + + + + +
"public.a_partition_table"
a_key1: integer
a_key2: timestamp without time zone
> ] "public.a_primary" [ label=< diff --git a/_examples/a_bit_of_everything/postgres/xo.xo.json b/_examples/a_bit_of_everything/postgres/xo.xo.json index 0368afde..bfeb24b1 100644 --- a/_examples/a_bit_of_everything/postgres/xo.xo.json +++ b/_examples/a_bit_of_everything/postgres/xo.xo.json @@ -857,6 +857,69 @@ ], "manual": true }, + { + "type":"table", + "name":"a_partition_table", + "columns":[ + { + "name":"a_key1", + "datatype":{ + "type":"integer" + }, + "is_primary":true + }, + { + "name":"a_key2", + "datatype":{ + "type":"timestamp without time zone" + }, + "is_primary":true + } + ], + "primary_keys":[ + { + "name":"a_key1", + "datatype":{ + "type":"integer" + }, + "is_primary":true + }, + { + "name":"a_key2", + "datatype":{ + "type":"timestamp without time zone" + }, + "is_primary":true + } + ], + "indexes":[ + { + "name":"a_partition_table_pkey", + "fields":[ + { + "name":"a_key1", + "datatype":{ + "type":"integer" + }, + "is_primary":true + }, + { + "name":"a_key2", + "datatype":{ + "type":"timestamp without time zone" + }, + "is_primary":true + } + ], + "is_unique":true, + "is_primary":true + } + ], + "manual":true, + "partition": { + "definition": "RANGE (a_key2)" + } + }, { "type": "table", "name": "a_primary", diff --git a/_examples/a_bit_of_everything/postgres/xo.xo.sql b/_examples/a_bit_of_everything/postgres/xo.xo.sql index a5115dc7..6d58e3e9 100644 --- a/_examples/a_bit_of_everything/postgres/xo.xo.sql +++ b/_examples/a_bit_of_everything/postgres/xo.xo.sql @@ -127,6 +127,14 @@ CREATE TABLE a_manual_table ( a_text VARCHAR(255) ); +-- table a_partition_table +CREATE TABLE a_partition_table ( + a_key1 INTEGER NOT NULL, + a_key2 TIMESTAMP NOT NULL, + PRIMARY KEY (a_key1, a_key2) +) +PARTITION BY RANGE (a_key2); + -- table a_primary_multi CREATE TABLE a_primary_multi ( a_key INTEGER NOT NULL, diff --git a/_examples/a_bit_of_everything/postgres/xo.xo.yaml b/_examples/a_bit_of_everything/postgres/xo.xo.yaml index e3ef6093..63924e8b 100644 --- a/_examples/a_bit_of_everything/postgres/xo.xo.yaml +++ b/_examples/a_bit_of_everything/postgres/xo.xo.yaml @@ -495,6 +495,42 @@ schemas: prec: 255 nullable: true manual: true + - type: table + name: a_partition_table + columns: + - name: a_key1 + datatype: + type: integer + is_primary: true + - name: a_key2 + datatype: + type: timestamp without time zone + is_primary: true + primary_keys: + - name: a_key1 + datatype: + type: integer + is_primary: true + - name: a_key2 + datatype: + type: timestamp without time zone + is_primary: true + indexes: + - name: a_partition_table_pkey + fields: + - name: a_key1 + datatype: + type: integer + is_primary: true + - name: a_key2 + datatype: + type: timestamp without time zone + is_primary: true + is_unique: true + is_primary: true + manual: true + partition: + definition: RANGE (a_key2) - type: table name: a_primary columns: diff --git a/_examples/a_bit_of_everything/sql/postgres_schema.sql b/_examples/a_bit_of_everything/sql/postgres_schema.sql index c90f1ff5..fab6876e 100644 --- a/_examples/a_bit_of_everything/sql/postgres_schema.sql +++ b/_examples/a_bit_of_everything/sql/postgres_schema.sql @@ -83,6 +83,13 @@ CREATE TABLE a_unique_index_composite ( UNIQUE (a_key1, a_key2) ); +-- table a_partition_table +CREATE TABLE a_partition_table ( + a_key1 INTEGER, + a_key2 TIMESTAMP, + CONSTRAINT a_partition_table_pkey PRIMARY KEY (a_key1,a_key2) +) PARTITION BY RANGE (a_key2); + -- enum type CREATE TYPE a_enum AS ENUM ( 'ONE', diff --git a/cmd/schema.go b/cmd/schema.go index 6eb9c409..99da2706 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -200,9 +200,13 @@ func LoadTables(ctx context.Context, args *Args, typ string) ([]xo.Table, error) } // create table t := &xo.Table{ - Type: typ, - Name: table.TableName, - Manual: true, + Type: typ, + Name: table.TableName, + Manual: true, + Partition: xo.Partition{ + Reference: table.PartitionOf, + Definition: table.PartitionDef, + }, Definition: strings.TrimSpace(table.ViewDef), } // process columns diff --git a/gen.sh b/gen.sh index 3835a367..6faf7a6f 100755 --- a/gen.sh +++ b/gen.sh @@ -152,21 +152,27 @@ $XOBIN query $PGDB -M -B -2 -T Table -F PostgresTables --type-comment "$COMMENT" SELECT (CASE c.relkind WHEN 'r' THEN 'table' + WHEN 'p' THEN 'table' WHEN 'v' THEN 'view' END)::varchar AS type, c.relname::varchar AS table_name, false::boolean AS manual_pk, CASE c.relkind WHEN 'r' THEN COALESCE(obj_description(c.relname::regclass), '') + WHEN 'p' THEN COALESCE(obj_description(c.relname::regclass), '') WHEN 'v' THEN v.definition - END AS view_def + END AS view_def, + (CASE WHEN i.inhparent IS NOT NULL THEN (SELECT pg_class.relname FROM pg_class WHERE pg_class.oid = i.inhparent) ELSE '' END) as partition_of, + COALESCE(pg_get_expr(c.relpartbound, c.oid, true), pg_get_partkeydef(c.oid), '') as partition_def FROM pg_class c JOIN ONLY pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_views v ON n.nspname = v.schemaname AND v.viewname = c.relname + LEFT JOIN pg_inherits i ON i.inhrelid = c.oid WHERE n.nspname = %%schema string%% AND (CASE c.relkind WHEN 'r' THEN 'table' + WHEN 'p' THEN 'table' WHEN 'v' THEN 'view' END) = LOWER(%%typ string%%) ENDSQL diff --git a/models/table.xo.go b/models/table.xo.go index 8dd72870..f8278d16 100644 --- a/models/table.xo.go +++ b/models/table.xo.go @@ -12,6 +12,8 @@ type Table struct { TableName string `json:"table_name"` // table_name ManualPk bool `json:"manual_pk"` // manual_pk ViewDef string `json:"view_def"` // view_def + PartitionOf string `json:"partition_of"` // partition_of + PartitionDef string `json:"partition_def"` // partition_def } // PostgresTables runs a custom query, returning results as [Table]. @@ -20,21 +22,27 @@ func PostgresTables(ctx context.Context, db DB, schema, typ string) ([]*Table, e const sqlstr = `SELECT ` + `(CASE c.relkind ` + `WHEN 'r' THEN 'table' ` + + `WHEN 'p' THEN 'table' ` + `WHEN 'v' THEN 'view' ` + `END), ` + // ::varchar AS type `c.relname, ` + // ::varchar AS table_name `false, ` + // ::boolean AS manual_pk `CASE c.relkind ` + `WHEN 'r' THEN COALESCE(obj_description(c.relname::regclass), '') ` + + `WHEN 'p' THEN COALESCE(obj_description(c.relname::regclass), '') ` + `WHEN 'v' THEN v.definition ` + - `END AS view_def ` + + `END AS view_def, ` + + `(CASE WHEN i.inhparent IS NOT NULL THEN (SELECT pg_class.relname FROM pg_class WHERE pg_class.oid = i.inhparent) ELSE '' END) as partition_of, ` + + `COALESCE(pg_get_expr(c.relpartbound, c.oid, true), pg_get_partkeydef(c.oid), '') as partition_def ` + `FROM pg_class c ` + `JOIN ONLY pg_namespace n ON n.oid = c.relnamespace ` + `LEFT JOIN pg_views v ON n.nspname = v.schemaname ` + `AND v.viewname = c.relname ` + + `LEFT JOIN pg_inherits i ON i.inhrelid = c.oid ` + `WHERE n.nspname = $1 ` + `AND (CASE c.relkind ` + `WHEN 'r' THEN 'table' ` + + `WHEN 'p' THEN 'table' ` + `WHEN 'v' THEN 'view' ` + `END) = LOWER($2)` // run @@ -49,7 +57,7 @@ func PostgresTables(ctx context.Context, db DB, schema, typ string) ([]*Table, e for rows.Next() { var t Table // scan - if err := rows.Scan(&t.Type, &t.TableName, &t.ManualPk, &t.ViewDef); err != nil { + if err := rows.Scan(&t.Type, &t.TableName, &t.ManualPk, &t.ViewDef, &t.PartitionOf, &t.PartitionDef); err != nil { return nil, logerror(err) } res = append(res, &t) diff --git a/templates/createdb/xo.xo.sql.tpl b/templates/createdb/xo.xo.sql.tpl index f92c8b01..669303bd 100644 --- a/templates/createdb/xo.xo.sql.tpl +++ b/templates/createdb/xo.xo.sql.tpl @@ -18,6 +18,7 @@ CREATE TYPE {{ esc $e.Name }} AS ENUM ( {{- if $s.Tables }} {{- range $t := $s.Tables }} -- table {{ $t.Name }} +{{- if eq $t.Partition.Reference ""}} CREATE TABLE {{ esc $t.Name }} ( {{- range $i, $c := $t.Columns }} {{ coldef $t $c }}{{ comma $i $t.Columns }} @@ -28,11 +29,24 @@ CREATE TABLE {{ esc $t.Name }} ( {{- range $fk := $t.ForeignKeys -}}{{- if gt (len $fk.Fields) 1 }}, {{ constraint $fk.Name -}} FOREIGN KEY ({{ fields $fk.Fields }}) REFERENCES {{ esc $fk.RefTable }} ({{ fields $fk.RefFields }}) {{- end -}}{{- end }} -){{ engine }}; +) +{{- if $t.Partition.Definition }} +PARTITION BY {{$t.Partition.Definition}} +{{- end -}} +{{- else }} +CREATE TABLE {{ esc $t.Name }} PARTITION OF {{ $t.Partition.Reference }} +{{$t.Partition.Definition}} +{{- end -}} +{{ engine }}; + {{- if $t.Indexes }} {{ range $idx := $t.Indexes }}{{ if not (or $idx.IsPrimary $idx.IsUnique) }} -- index {{ $idx.Name }} +{{- if $t.Partition.Reference }} +CREATE INDEX IF NOT EXISTS {{ esc $idx.Name }} ON {{ esc $t.Name }} ({{ fields $idx.Fields }}); +{{ else }} CREATE INDEX {{ esc $idx.Name }} ON {{ esc $t.Name }} ({{ fields $idx.Fields }}); +{{ end -}} {{ end -}}{{- end -}}{{- end }} {{ end -}} {{- end -}} diff --git a/types/types.go b/types/types.go index adc30869..57c80320 100644 --- a/types/types.go +++ b/types/types.go @@ -98,6 +98,7 @@ type Table struct { ForeignKeys []ForeignKey `json:"foreign_keys,omitempty"` Manual bool `json:"manual,omitempty"` Definition string `json:"definition,omitempty"` // empty for tables + Partition Partition `json:"partition,omitempty"` // empty for views } // MarshalYAML satisfies the yaml.Marshaler interface. @@ -150,6 +151,12 @@ type Type struct { Enum *Enum `json:"-"` } +// Partition holds information about table partition. +type Partition struct { + Reference string `json:"reference,omitempty"` + Definition string `json:"definition,omitempty"` +} + // ParseType parses "type[ (precision[,scale])][\[\]]" strings returning the // parsed precision, scale, and if the type is an array or not. //