#!/usr/bin/env perl
#
# Copyright (C) 2001-2025 Zabbix SIA
#
# This program is free software: you can redistribute it and/or modify it under the terms of
# the GNU Affero General Public License as published by the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with this program.
# If not, see .
use strict;
use warnings;
use File::Basename;
my $file = dirname($0) . "/../src/schema.tmpl"; # name the file
my ($state, %output, $eol, $fk_bol, $fk_eol, $ltab, $pkey, $table_name, $pkey_name);
my ($szcol1, $szcol2, $szcol3, $szcol4, $sequences, $sql_suffix, $triggers);
my ($fkeys, $fkeys_prefix, $fkeys_suffix, $uniq, $delete_cascade);
my %table_types; # for making sure that table types aren't duplicated
my %c = (
"type" => "code",
"database" => "",
"after" => "\t{0}\n};\n\n#undef ZBX_TYPE_LONGTEXT_LEN\n#undef ZBX_TYPE_SHORTTEXT_LEN\n",
"t_bigint" => "ZBX_TYPE_UINT",
"t_text" => "ZBX_TYPE_TEXT",
"t_double" => "ZBX_TYPE_FLOAT",
"t_id" => "ZBX_TYPE_ID",
"t_image" => "ZBX_TYPE_BLOB",
"t_bin" => "ZBX_TYPE_BLOB",
"t_integer" => "ZBX_TYPE_INT",
"t_longtext" => "ZBX_TYPE_LONGTEXT",
"t_nanosec" => "ZBX_TYPE_INT",
"t_serial" => "ZBX_TYPE_UINT",
"t_shorttext" => "ZBX_TYPE_SHORTTEXT",
"t_time" => "ZBX_TYPE_INT",
"t_varchar" => "ZBX_TYPE_CHAR",
"t_cuid" => "ZBX_TYPE_CUID",
);
$c{"before"} = "/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see .
**/
#include \"zbxdbschema.h\"
#include \"zbxcommon.h\"
#if defined(HAVE_ORACLE)
# define ZBX_TYPE_SHORTTEXT_LEN 2048
#else
# define ZBX_TYPE_SHORTTEXT_LEN 65535
#endif
#define ZBX_TYPE_LONGTEXT_LEN 0
#define ZBX_TYPE_TEXT_LEN 65535
static zbx_db_table_t\ttables[] = {
";
my %mysql = (
"type" => "sql",
"database" => "mysql",
"before" => "",
"after" => "",
"table_options" => " ENGINE=InnoDB",
"t_bigint" => "bigint unsigned",
"t_text" => "text",
"t_double" => "DOUBLE PRECISION",
"t_id" => "bigint unsigned",
"t_image" => "longblob",
"t_bin" => "longblob",
"t_integer" => "integer",
"t_longtext" => "longtext",
"t_nanosec" => "integer",
"t_serial" => "bigint unsigned",
"t_shorttext" => "text",
"t_time" => "integer",
"t_varchar" => "varchar",
"t_cuid" => "varchar(25)",
);
my %oracle = (
"type" => "sql",
"database" => "oracle",
"before" => "",
"after" => "",
"table_options" => "",
"t_bigint" => "number(20)",
"t_text" => "nclob",
"t_double" => "BINARY_DOUBLE",
"t_id" => "number(20)",
"t_image" => "blob",
"t_bin" => "blob",
"t_integer" => "number(10)",
"t_longtext" => "nclob",
"t_nanosec" => "number(10)",
"t_serial" => "number(20)",
"t_shorttext" => "nvarchar2(2048)",
"t_time" => "number(10)",
"t_varchar" => "nvarchar2",
"t_cuid" => "nvarchar2(25)",
);
my %postgresql = (
"type" => "sql",
"database" => "postgresql",
"before" => "",
"after" => "",
"table_options" => "",
"t_bigint" => "numeric(20)",
"t_text" => "text",
"t_double" => "DOUBLE PRECISION",
"t_id" => "bigint",
"t_image" => "bytea",
"t_bin" => "bytea",
"t_integer" => "integer",
"t_longtext" => "text",
"t_nanosec" => "integer",
"t_serial" => "bigserial",
"t_shorttext" => "text",
"t_time" => "integer",
"t_varchar" => "varchar",
"t_cuid" => "varchar(25)",
);
my %sqlite3 = (
"type" => "sql",
"database" => "sqlite3",
"before" => "",
"after" => "",
"table_options" => "",
"t_bigint" => "bigint",
"t_text" => "text",
"t_double" => "DOUBLE PRECISION",
"t_id" => "bigint",
"t_image" => "longblob",
"t_bin" => "longblob",
"t_integer" => "integer",
"t_longtext" => "text",
"t_nanosec" => "integer",
"t_serial" => "integer",
"t_shorttext" => "text",
"t_time" => "integer",
"t_varchar" => "varchar",
"t_cuid" => "varchar(25)",
);
sub rtrim($)
{
my $string = shift;
$string =~ s/(\r|\n)+$// if ($string);
return $string;
}
sub newstate($)
{
my $new = shift;
if ($state eq "field")
{
if ($output{"type"} eq "sql" && ($new eq "index" || $new eq "table" || $new eq "row"))
{
print "${pkey}${eol}\n)$output{'table_options'};${eol}\n";
}
if ($new eq "field")
{
print ",${eol}\n";
}
}
if ($state ne "bof")
{
if ($output{"type"} eq "code" && $new eq "table")
{
if ($uniq ne "")
{
print ",\n\t\t\t{0}\n\t\t}${uniq}\n\t},\n";
$uniq = "";
}
else
{
print ",\n\t\t\t{0}\n\t\t},\n\t\tNULL\n\t},\n";
}
}
}
$state = $new;
}
sub process_table($)
{
my $line = shift;
my $flags;
newstate("table");
$delete_cascade = 0;
($table_name, $pkey_name, $flags) = split(/\|/, $line, 3);
if ($output{"type"} eq "code")
{
if ($flags eq "")
{
$flags = "0";
}
for ($flags)
{
# do not output ZBX_DATA ZBX_DASHBOARD and ZBX_TEMPLATE, remove it
s/ZBX_DATA//;
s/ZBX_TEMPLATE//;
s/ZBX_DASHBOARD//;
s/,+$//;
s/^,+//;
s/,+/ \| /g;
s/^$/0/;
}
print "\t{\"${table_name}\", \"${pkey_name}\", ${flags},\n\t\t{\n";
}
else
{
if ($pkey_name ne "")
{
$pkey = ",${eol}\n${ltab}PRIMARY KEY (${pkey_name})";
}
else
{
$pkey = "";
}
if ($output{"database"} eq "mysql")
{
print "CREATE TABLE `${table_name}` (${eol}\n";
}
else
{
print "CREATE TABLE ${table_name} (${eol}\n";
}
}
}
sub process_field($)
{
my $line = shift;
newstate("field");
my ($name, $type, $default, $null, $flags, $relN, $fk_table, $fk_field, $fk_flags) = split(/\|/, $line, 9);
my ($type_short, $length) = $type =~ /^(\w+)(?:\((\d+)\))?$/;
if ($output{"type"} eq "code")
{
$type = $output{$type_short};
if ($type eq "ZBX_TYPE_CHAR")
{
# use specified $length, don't override it
}
elsif ($type eq "ZBX_TYPE_TEXT")
{
$length = "ZBX_TYPE_TEXT_LEN";
}
elsif ($type eq "ZBX_TYPE_SHORTTEXT")
{
$length = "ZBX_TYPE_SHORTTEXT_LEN";
}
elsif ($type eq "ZBX_TYPE_LONGTEXT")
{
$length = "ZBX_TYPE_LONGTEXT_LEN";
}
elsif ($type eq "ZBX_TYPE_CUID")
{
$length = 0;
}
else
{
$length = 0;
}
for ($flags)
{
# do not output ZBX_NODATA, remove it
s/ZBX_NODATA//;
s/,+$//;
s/^,+//;
s/,+/ \| /g;
s/^$/0/;
}
if ($null eq "NOT NULL")
{
if ($flags ne "0")
{
$flags = "ZBX_NOTNULL | ${flags}";
}
else
{
$flags = "ZBX_NOTNULL";
}
}
$flags =~ s/,/ \| /g;
if ($fk_table)
{
if (not $fk_field or $fk_field eq "")
{
$fk_field = $name;
}
$fk_table = "\"${fk_table}\"";
$fk_field = "\"${fk_field}\"";
if (not $fk_flags or $fk_flags eq "")
{
$delete_cascade = 1;
$fk_flags = "ZBX_FK_CASCADE_DELETE";
}
elsif ($fk_flags eq "RESTRICT")
{
$fk_flags = "0";
}
}
else
{
$fk_table = "NULL";
$fk_field = "NULL";
$fk_flags = "0";
}
if ($default eq "")
{
$default = "NULL";
}
else
{
$default =~ s/'//g;
$default = "\"$default\"";
}
print "\t\t\t{\"${name}\", ${default}, ${fk_table}, ${fk_field}, ${length}, $type, ${flags}, ${fk_flags}}";
}
else
{
my @text_fields;
$type =~ s/$type_short/$output{$type_short}/g;
if (($output{"database"} eq "oracle") && (index($type, "nvarchar2") == 0 || index($type, "nclob") == 0))
{
$null = "";
}
my $row = $null;
if ($type_short eq "t_serial")
{
if ($output{"database"} eq "sqlite3")
{
$row = sprintf("%-*s PRIMARY KEY AUTOINCREMENT", $szcol4, $row);
$pkey = "";
}
elsif ($output{"database"} eq "mysql")
{
$row = sprintf("%-*s auto_increment", $szcol4, $row);
}
elsif ($output{"database"} eq "oracle")
{
$sequences .= "CREATE SEQUENCE ${table_name}_seq${eol}\n";
$sequences .= "START WITH 1${eol}\n";
$sequences .= "INCREMENT BY 1${eol}\n";
$sequences .= "NOMAXVALUE${eol}\n/${eol}\n";
$sequences .= "CREATE TRIGGER ${table_name}_tr${eol}\n";
$sequences .= "BEFORE INSERT ON ${table_name}${eol}\n";
$sequences .= "FOR EACH ROW${eol}\n";
$sequences .= "BEGIN${eol}\n";
$sequences .= "SELECT ${table_name}_seq.nextval INTO :new.${name} FROM dual;${eol}\n";
$sequences .= "END;${eol}\n/${eol}\n";
}
}
my $references = "";
if ($relN and $relN ne "" and $relN ne "-")
{
my $only = "";
if (not $fk_field or $fk_field eq "")
{
$fk_field = $name;
}
# RESTRICT may contain new line chars, we need to clean them out
$fk_flags = rtrim($fk_flags);
if (not $fk_flags or $fk_flags eq "")
{
$delete_cascade = 1;
$fk_flags = " ON DELETE CASCADE";
}
elsif ($fk_flags eq "RESTRICT")
{
$fk_flags = "";
}
if ($output{"database"} eq "postgresql")
{
$only = " ONLY";
}
my $cname = "c_${table_name}_${relN}";
if ($output{"database"} eq "sqlite3")
{
$references = " REFERENCES ${fk_table} (${fk_field})${fk_flags}";
}
else
{
$references = "";
if ($output{"database"} eq "mysql")
{
$fkeys .= "${fk_bol}ALTER TABLE${only} `${table_name}` ADD CONSTRAINT `${cname}` FOREIGN KEY (`${name}`) REFERENCES `${fk_table}` (`${fk_field}`)${fk_flags}${fk_eol}\n";
}
else
{
$fkeys .= "${fk_bol}ALTER TABLE${only} ${table_name} ADD CONSTRAINT ${cname} FOREIGN KEY (${name}) REFERENCES ${fk_table} (${fk_field})${fk_flags}${fk_eol}\n";
}
}
}
if ($output{"database"} eq "mysql")
{
@text_fields = ('blob', 'longblob', 'text', 'longtext');
$default = "" if (grep /$output{$type_short}/, @text_fields);
$name = "`${name}`";
}
if ($default ne "")
{
$default = "DEFAULT $default";
}
printf("${ltab}%-*s %-*s %-*s ${row}${references}", $szcol1, $name, $szcol2, $type, $szcol3, $default);
}
}
sub process_index($$)
{
my $line = shift;
my $unique = shift;
newstate("index");
my ($name, $fields) = split(/\|/, $line, 2);
if ($output{"type"} eq "code")
{
if (1 == $unique)
{
$uniq = ",\n\t\t\"${fields}\"";
}
}
else
{
if (1 == $unique)
{
$unique = " UNIQUE";
}
else
{
$unique = "";
}
if ($output{"database"} eq "mysql")
{
$fields =~ s/,/`,`/g;
my $quote_index = "`$fields`";
$quote_index =~ s/\)`/\)/g;
$quote_index =~ s/\(/`\(/g;
print "CREATE${unique} INDEX `${table_name}_$name` ON `$table_name` ($quote_index);${eol}\n";
}
else
{
$fields =~ s/\(\d+\)//g;
print "CREATE${unique} INDEX ${table_name}_$name ON $table_name ($fields);${eol}\n";
}
}
}
sub process_row($)
{
my $line = shift;
newstate("row");
my @array = split(/\|/, $line);
my $first = 1;
my $values = "(";
foreach (@array)
{
$values .= "," if ($first == 0);
$first = 0;
# remove leading and trailing spaces
$_ =~ s/^\s+//;
$_ =~ s/\s+$//;
if ($_ eq 'NULL')
{
$values .= $_;
}
else
{
my $modifier = '';
# escape backslashes
if (/\\/)
{
if ($output{'database'} eq 'postgresql')
{
$_ =~ s/\\/\\\\/g;
$modifier = 'E';
}
elsif ($output{'database'} eq 'mysql')
{
$_ =~ s/\\/\\\\/g;
}
}
# escape single quotes
if (/'/)
{
if ($output{'database'} eq 'mysql')
{
$_ =~ s/'/\\'/g;
}
else
{
$_ =~ s/'/''/g;
}
}
$_ =~ s/&pipe;/|/g;
if ($output{'database'} eq 'mysql' || $output{'database'} eq 'oracle')
{
$_ =~ s/&eol;/\\r\\n/g;
}
else
{
$_ =~ s/&eol;/\x0D\x0A/g;
}
$values .= "${modifier}'${_}'";
}
}
$values .= ")";
print "INSERT INTO $table_name VALUES $values;${eol}\n";
}
sub timescaledb_get_version($)
{
my $constant_name = shift;
my $version_file = dirname($0) . "/../../include/zbx_dbversion_constants.h";
if (! -r $version_file)
{
print "Expected file \"$version_file\" not found\n";
exit;
}
my $ver = `grep $constant_name $version_file`;
chomp($ver);
(undef, undef, $ver) = split(' ', $ver);
$ver =~ m/"([^\.]+)\.([^\.]+)[\."]/;
return ($1, $2);
}
sub timescaledb()
{
my ($minimum_postgres_version_major, $minimum_postgres_version_minor) =
timescaledb_get_version("ZBX_POSTGRESQL_MIN_VERSION_STR");
my ($minimum_timescaledb_version_major, $minimum_timescaledb_version_minor) =
timescaledb_get_version("ZBX_TIMESCALE_MIN_VERSION_STR");
print< true, if_not_exists => true";
for ("history", "history_uint", "history_log", "history_text", "history_str", "history_bin")
{
print< 86400, $flags);
EOF
;
}
print< 604800,
time_partitioning_func => 'cuid_timestamp', $flags);
EOF
;
for ("trends", "trends_uint")
{
print< 2592000, $flags);
EOF
;
}
print<; # read it into an array
close(INFO); # close the file
foreach my $line (@lines)
{
$line =~ tr/\t//d;
chop($line);
my ($type, $opts) = split(/\|/, $line, 2);
if ($type)
{
if ($type eq 'FIELD') { process_field($opts); }
elsif ($type eq 'INDEX') { process_index($opts, 0); }
elsif ($type eq 'TABLE') { process_table($opts); }
elsif ($type eq 'UNIQUE') { process_index($opts, 1); }
elsif ($type eq 'CHANGELOG') { process_changelog($opts); }
elsif ($type eq 'UPD_TRIG_FUNC')
{
process_update_trigger_function($opts);
}
elsif ($type eq 'ROW' && $output{"type"} ne "code") { process_row($opts); }
}
}
newstate("table");
if ($output{"database"} eq "mysql")
{
print "DELIMITER \$\$${eol}\n";
}
print $sequences . $triggers . $sql_suffix;
if ($output{"database"} eq "mysql")
{
print "DELIMITER ;${eol}\n";
}
print $fkeys_prefix . $fkeys . $fkeys_suffix;
print $output{"after"};
}
sub c_append_changelog_tables()
{
print "
static const zbx_db_table_changelog_t\tchangelog_tables[] =
{\n";
while (my ($object, $table) = each(%table_types)) {
print "\t{\"$table\", $object},\n"
}
print "\t{0}\n};\n";
}
sub main()
{
if ($#ARGV != 0)
{
usage();
}
$eol = "";
$fk_bol = "";
$fk_eol = ";";
$ltab = "\t";
$szcol1 = 24;
$szcol2 = 15;
$szcol3 = 25;
$szcol4 = 7;
$sql_suffix = "";
$fkeys_prefix = "";
$fkeys_suffix = "";
my $format = $ARGV[0];
if ($format eq 'c') { %output = %c; }
elsif ($format eq 'mysql') { %output = %mysql; }
elsif ($format eq 'oracle') { %output = %oracle; }
elsif ($format eq 'postgresql') { %output = %postgresql; }
elsif ($format eq 'sqlite3') { %output = %sqlite3; }
elsif ($format eq 'timescaledb') { timescaledb(); }
else { usage(); }
process();
if ($format eq "c")
{
c_append_changelog_tables();
$eol = "\\n\\";
$fk_bol = "\t\"";
$fk_eol = "\",";
$ltab = "";
$szcol1 = 0;
$szcol2 = 0;
$szcol3 = 0;
$szcol4 = 0;
$sql_suffix="\";\n";
print "#if defined(HAVE_SQLITE3)\nstatic const char\t*db_schema = \"\\\n";
%output = %sqlite3;
process();
print "#else\t/* HAVE_SQLITE3 */\n";
print "static const char\t*db_schema = NULL;\n";
print "#endif\t/* not HAVE_SQLITE3 */\n";
print "\nzbx_db_table_t\t*zbx_dbschema_get_tables(void)\n{\n\treturn tables;\n}\n";
print "\nconst zbx_db_table_changelog_t\t*zbx_dbschema_get_changelog_tables(void)\n" .
"{\n\treturn changelog_tables;\n}\n";
print "\nconst char\t*zbx_dbschema_get_schema(void)\n{\n\treturn db_schema;\n}\n";
}
}
main();