diff --git a/Changelog.md b/Changelog.md index e6609ab..4c16af2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.115] - 2026-05-8 +### Added +- support for partitioned tables + +## [1.114] +### Fixed +- create index with include and where + +## [1.113] +### Fixed +- recreate_postgres single view with dependencies + +## [1.112] +### Fixed +- schema:generate - integer column on postgresql + +# [1.110] +### Fixed +- create procedures order + +# [1.109] +### Added +- Support index include columns +- View creation with topological sort +### Fixed +- schema:recreate_postgres function order + ## [1.108] - 2025-06-26 ### Fixed - check constraints on postgresql 17 diff --git a/build.gradle b/build.gradle index de8a86d..c059b83 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ mainClassName = "br.com.bluesoft.bee.Bee" group = 'br.com.bluesoft.bee' def artifact = 'bee' -version = '1.114' +version = '1.115' def javaVersion = JavaVersion.VERSION_1_8 sourceCompatibility = javaVersion; diff --git a/src/main/groovy/br/com/bluesoft/bee/database/reader/PostgresDatabaseReader.groovy b/src/main/groovy/br/com/bluesoft/bee/database/reader/PostgresDatabaseReader.groovy index 692d7b0..d86953c 100644 --- a/src/main/groovy/br/com/bluesoft/bee/database/reader/PostgresDatabaseReader.groovy +++ b/src/main/groovy/br/com/bluesoft/bee/database/reader/PostgresDatabaseReader.groovy @@ -43,6 +43,7 @@ import br.com.bluesoft.bee.model.Schema import br.com.bluesoft.bee.model.Sequence import br.com.bluesoft.bee.model.Table import br.com.bluesoft.bee.model.TableColumn +import br.com.bluesoft.bee.model.TablePartition import br.com.bluesoft.bee.model.Trigger import br.com.bluesoft.bee.model.View import br.com.bluesoft.bee.util.VersionHelper @@ -87,23 +88,30 @@ class PostgresDatabaseReader implements DatabaseReader { fillIndexes(tables, objectName, databaseVersion) fillCostraints(tables, objectName, databaseVersion) fillCostraintsColumns(tables, objectName) + fillPartitions(tables, objectName) return tables } static final def TABLES_QUERY = ''' - select t.table_name, 'N'as temporary, description + select t.table_name, 'N'as temporary, description, + pg_get_partkeydef(to_regclass(t.table_name)::regclass::oid) partitioned from information_schema.tables t left join pg_description d on d.objoid = to_regclass(t.table_name)::regclass::oid - where t.table_type = 'BASE TABLE' and table_schema not in ('pg_catalog', 'information_schema') - order by table_name + left join pg_inherits i on i.inhrelid = to_regclass(t.table_name)::regclass::oid + where t.table_type = 'BASE TABLE' and table_schema not in ('pg_catalog', 'information_schema') + and i.inhrelid is null + order by table_name ''' static final def TABLES_QUERY_BY_NAME = ''' - select t.table_name, 'N'as temporary, description + select t.table_name, 'N'as temporary, description, + pg_get_partkeydef(to_regclass(t.table_name)::regclass::oid) partitioned from information_schema.tables t left join pg_description d on d.objoid = to_regclass(t.table_name)::regclass::oid - where t.table_type = 'BASE TABLE' and table_schema not in ('pg_catalog', 'information_schema') - and t.table_name = ? - order by table_name + left join pg_inherits i on i.inhrelid = to_regclass(t.table_name)::regclass::oid + where t.table_type = 'BASE TABLE' and table_schema not in ('pg_catalog', 'information_schema') + and i.inhrelid is null + and t.table_name like ? + order by table_name ''' private def fillTables(objectName) { @@ -118,7 +126,8 @@ class PostgresDatabaseReader implements DatabaseReader { def name = it.table_name.toLowerCase() def temporary = it.temporary == 'Y' ? true : false def comment = it.description - tables[name] = new Table(name: name, temporary: temporary, comment: comment) + def partitioned = it.partitioned + tables[name] = new Table(name: name, temporary: temporary, comment: comment, partitioned: partitioned) }) return tables } @@ -172,19 +181,21 @@ class PostgresDatabaseReader implements DatabaseReader { } rows.each({ def table = tables[it.table_name.toLowerCase()] - def column = new TableColumn() - column.name = it.column_name.toLowerCase() - column.type = getColumnType(it.data_type) - column.size = it.data_size - column.scale = it.data_scale == null ? 0 : it.data_scale - column.nullable = it.nullable == 'NO' ? false : true - column.virtual = it.is_generated == 'ALWAYS' - column.comment = it.comments - def defaultValue = it.data_default - if (defaultValue) { - column.defaultValue = defaultValue?.trim()?.toUpperCase() == 'NULL' ? null : defaultValue?.trim() + if (table) { + def column = new TableColumn() + column.name = it.column_name.toLowerCase() + column.type = getColumnType(it.data_type) + column.size = it.data_size + column.scale = it.data_scale == null ? 0 : it.data_scale + column.nullable = it.nullable == 'NO' ? false : true + column.virtual = it.is_generated == 'ALWAYS' + column.comment = it.comments + def defaultValue = it.data_default + if (defaultValue) { + column.defaultValue = defaultValue?.trim()?.toUpperCase() == 'NULL' ? null : defaultValue?.trim() + } + table.columns[column.name] = column } - table.columns[column.name] = column }) } @@ -419,6 +430,41 @@ class PostgresDatabaseReader implements DatabaseReader { }) } + final static def PARTITIONS_QUERY = ''' + select i.inhparent::regclass::text parent, i.inhrelid::regclass::text table, + pg_catalog.pg_get_expr(c.relpartbound, c.oid) partition + from pg_catalog.pg_inherits i + join pg_catalog.pg_class c on c.oid = i.inhrelid + where c.relkind = 'r' + order by pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'default', c.oid::pg_catalog.regclass::pg_catalog.text + ''' + + final static def PARTITIONS_BY_TABLE_QUERY = ''' + select i.inhparent::regclass::text parent, i.inhrelid::regclass::text table, + pg_catalog.pg_get_expr(c.relpartbound, c.oid) partition + from pg_catalog.pg_inherits i + join pg_catalog.pg_class c on c.oid = i.inhrelid + where c.relkind = 'r' + and i.inhparent::regclass::text = ? + order by pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'default', c.oid::pg_catalog.regclass::pg_catalog.text + ''' + + private def fillPartitions(tables, objectName) { + def rows + if (objectName) { + rows = sql.rows(PARTITIONS_BY_TABLE_QUERY, [objectName]) + } else { + rows = sql.rows(PARTITIONS_QUERY) + } + + rows.each { + def table = tables[it.parent.toLowerCase()] + if (table) { + table.partitions << new TablePartition(name: it.table, value: it.partition) + } + } + } + final static def SEQUENCES_QUERY = ''' select c.relname as sequence_name, '1' as min_value from pg_class c diff --git a/src/main/groovy/br/com/bluesoft/bee/model/Table.groovy b/src/main/groovy/br/com/bluesoft/bee/model/Table.groovy index b7ecc82..3fe0e9b 100644 --- a/src/main/groovy/br/com/bluesoft/bee/model/Table.groovy +++ b/src/main/groovy/br/com/bluesoft/bee/model/Table.groovy @@ -39,13 +39,15 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect import groovy.transform.AutoClone @AutoClone -@com.fasterxml.jackson.annotation.JsonAutoDetect(isGetterVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.ANY) +@JsonAutoDetect(isGetterVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.ANY) class Table implements Validator { String name Boolean temporary = false String comment String distStyle + String partitioned + List partitions = [] Map columns = [:] as LinkedHashMap Map indexes = [:] diff --git a/src/main/groovy/br/com/bluesoft/bee/model/TablePartition.java b/src/main/groovy/br/com/bluesoft/bee/model/TablePartition.java new file mode 100644 index 0000000..f795c78 --- /dev/null +++ b/src/main/groovy/br/com/bluesoft/bee/model/TablePartition.java @@ -0,0 +1,22 @@ +package br.com.bluesoft.bee.model; + +public class TablePartition { + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/main/groovy/br/com/bluesoft/bee/schema/BeePostgresSchemaCreator.groovy b/src/main/groovy/br/com/bluesoft/bee/schema/BeePostgresSchemaCreator.groovy index 6da9fb6..446702a 100644 --- a/src/main/groovy/br/com/bluesoft/bee/schema/BeePostgresSchemaCreator.groovy +++ b/src/main/groovy/br/com/bluesoft/bee/schema/BeePostgresSchemaCreator.groovy @@ -5,6 +5,16 @@ import br.com.bluesoft.bee.util.CsvUtil class BeePostgresSchemaCreator extends BeeSchemaCreator { + def createTablePartitions(def table) { + def result = "" + + table.partitions.each { + result += "create table ${it.name} partition of ${table.name} ${it.value};\n" + } + + return result + } + def createColumn(def column) { def result = " ${column.name} ${column.type}" if (column.type in ['character', 'character varying']) { diff --git a/src/main/groovy/br/com/bluesoft/bee/schema/BeeSchemaCreator.groovy b/src/main/groovy/br/com/bluesoft/bee/schema/BeeSchemaCreator.groovy index 951d5b8..19bf2d3 100644 --- a/src/main/groovy/br/com/bluesoft/bee/schema/BeeSchemaCreator.groovy +++ b/src/main/groovy/br/com/bluesoft/bee/schema/BeeSchemaCreator.groovy @@ -42,6 +42,8 @@ abstract class BeeSchemaCreator { return result } + def createTablePartitions(def table) { + } def createTable(def table) { def columns = [] @@ -49,7 +51,10 @@ abstract class BeeSchemaCreator { columns << createColumn(it.value) }) def temp = table.temporary ? " global temporary" : "" - def result = "create${temp} table ${table.name} (\n" + columns.join(",\n") + "\n);\n" + def partition = table.partitioned ? " partition by ${table.partitioned}" : "" + def result = "create${temp} table ${table.name} (\n" + columns.join(",\n") + "\n)${partition};\n" + + result += createTablePartitions(table) if (table.comment) { result += "comment on table ${table.name} is '${table.comment}';\n"