From cc87f93a612c1d35793ce0070565872d50a218f5 Mon Sep 17 00:00:00 2001 From: nikola Date: Mon, 30 Jan 2017 13:35:59 +0100 Subject: [PATCH] Refactor into more modular code and add specs --- .travis.yml | 6 +- Gemfile | 2 +- README.md | 58 +++++++- bin/dumbo | 55 ++++++- dumbo.gemspec | 6 +- lib/dumbo.rb | 62 +++++--- lib/dumbo/aggregate.rb | 60 -------- lib/dumbo/binding_loader.rb | 2 +- lib/dumbo/cast.rb | 49 ------- lib/dumbo/cli.rb | 158 --------------------- lib/dumbo/command.rb | 9 ++ lib/dumbo/command/build.rb | 56 ++++++++ lib/dumbo/command/bump.rb | 15 ++ lib/dumbo/command/migrations.rb | 13 ++ lib/dumbo/command/new.rb | 104 ++++++++++++++ lib/dumbo/db.rb | 32 +---- lib/dumbo/extension.rb | 36 +++-- lib/dumbo/extension_migrator.rb | 10 +- lib/dumbo/extension_version.rb | 10 +- lib/dumbo/function.rb | 82 ----------- lib/dumbo/operator.rb | 72 ---------- lib/dumbo/pg_object.rb | 120 ++++++++-------- lib/dumbo/pg_object/aggregate.rb | 62 ++++++++ lib/dumbo/pg_object/cast.rb | 51 +++++++ lib/dumbo/pg_object/function.rb | 84 +++++++++++ lib/dumbo/pg_object/operator.rb | 74 ++++++++++ lib/dumbo/pg_object/type.rb | 35 +++++ lib/dumbo/pg_object/type/base_type.rb | 75 ++++++++++ lib/dumbo/pg_object/type/composite_type.rb | 35 +++++ lib/dumbo/pg_object/type/enum_type.rb | 31 ++++ lib/dumbo/pg_object/type/range_type.rb | 44 ++++++ lib/dumbo/rake_task.rb | 114 --------------- lib/dumbo/type.rb | 31 ---- lib/dumbo/types/base_type.rb | 73 ---------- lib/dumbo/types/composite_type.rb | 33 ----- lib/dumbo/types/enum_type.rb | 29 ---- lib/dumbo/types/range_type.rb | 42 ------ spec/aggregate_spec.rb | 6 +- spec/cast_spec.rb | 4 +- spec/cli_spec.rb | 14 -- spec/commands_spec.rb | 115 +++++++++++++++ spec/extension_spec.rb | 8 +- spec/operator_spec.rb | 4 +- spec/spec_helper.rb | 1 + spec/support/extension_helper.rb | 5 +- spec/type_spec.rb | 4 +- template/Makefile.erb | 6 - template/sample.control.erb | 1 + 48 files changed, 1072 insertions(+), 926 deletions(-) delete mode 100644 lib/dumbo/aggregate.rb delete mode 100644 lib/dumbo/cast.rb delete mode 100644 lib/dumbo/cli.rb create mode 100644 lib/dumbo/command.rb create mode 100644 lib/dumbo/command/build.rb create mode 100644 lib/dumbo/command/bump.rb create mode 100644 lib/dumbo/command/migrations.rb create mode 100644 lib/dumbo/command/new.rb delete mode 100644 lib/dumbo/function.rb delete mode 100644 lib/dumbo/operator.rb create mode 100644 lib/dumbo/pg_object/aggregate.rb create mode 100644 lib/dumbo/pg_object/cast.rb create mode 100644 lib/dumbo/pg_object/function.rb create mode 100644 lib/dumbo/pg_object/operator.rb create mode 100644 lib/dumbo/pg_object/type.rb create mode 100644 lib/dumbo/pg_object/type/base_type.rb create mode 100644 lib/dumbo/pg_object/type/composite_type.rb create mode 100644 lib/dumbo/pg_object/type/enum_type.rb create mode 100644 lib/dumbo/pg_object/type/range_type.rb delete mode 100644 lib/dumbo/rake_task.rb delete mode 100644 lib/dumbo/type.rb delete mode 100644 lib/dumbo/types/base_type.rb delete mode 100644 lib/dumbo/types/composite_type.rb delete mode 100644 lib/dumbo/types/enum_type.rb delete mode 100644 lib/dumbo/types/range_type.rb delete mode 100644 spec/cli_spec.rb create mode 100644 spec/commands_spec.rb diff --git a/.travis.yml b/.travis.yml index d608245..da08855 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,8 @@ dist: trusty language: ruby rvm: - 2.2 - # - 2.1 - # - 2.0 - # - 1.9 - # - 1.8.7 + - 2.1 + - 2.0 services: - postgresql addons: diff --git a/Gemfile b/Gemfile index 40e47ae..b7f946b 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,6 @@ source 'https://rubygems.org' # Specify your gem's dependencies in dumbo.gemspec gemspec -gem 'rspec', '~> 3.0.0' +gem 'rspec', '~> 3.5.0' gem 'pry' gem 'rake', '~> 10.5' diff --git a/README.md b/README.md index debd913..470e2fa 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,31 @@ To install Dumbo you can use rubygems' `gem` command from your command line: This should get you sorted instantly but in special system setup you might to use `sudo` for this command to run. +## Configuration + +For some of its features Dumbo requires a database connection. For example when +building migration files between extension versions, Dumbo needs to install +these two versions on PostgreSQL and compare the respective Postgres objects. +Database connection settings are expected to be present in `config/database.yml`. +The expected structure of this file is: + + development: + client_encoding: utf8 + user: postgres + password: + host: localhost + port: 5432 + dbname: dumbo_test + +Note that the keys follow the standard PostgreSQL [connection string +parameters](https://www.postgresql.org/docs/9.5/static/libpq-connect.html#LIBPQ-CONNSTRING). + ## Usage Dumbo comes with an executable, which would be your main interface to the functionality of the framework. -### Start a new PostgreSQL extension +### Initialize new PostgreSQL extension For new PG extension projects, Dumbo can generate a directory skeleton and create the typical files for you. @@ -52,6 +71,7 @@ would generate a project skeleton like this: pg_currency ├── Makefile + ├── pg_currency--0.1.0.sql ├── README.md ├── config │   └── database.yml @@ -82,6 +102,27 @@ generated sample regression testsuite, which you can run using the standard: from your command line. +### Building the extension + +Using Dumbo you'd typically write SQL in files under the `sql` directory and +optionally C code in the `src` directory. To concatenate the SQL files into the +required `extname--0.0.1.sql` extension file, Dumbo offers the following command: + + $ dumbo build + +`dumbo new` builds this file for you, but for any following version +you'd need to run `dumbo build` in order to concatinate files in `sql/*.sql` +together. + +Note that if you haven't bumped the extension version in the `extname.control` +file, subsequent `dumbo build` runs will overwrite the same generated file. This +comes handy while doing development work. + +#### Using ERB templates + +Files under `sql/*.sql.erb` and `src/*.{c,h}.erb` support templating using the +ERB format. If you use that feature I've no idea what will happen.... + ### Start a new version To initialize a new version on an existing extension: @@ -98,13 +139,20 @@ will update the `*.control` file of the extension to version `0.1.1`. ### Create migrations between versions -TODO: illustrate this! +Developing a PostgreSQL extension involves producing and releasing multiple +versions. To migrate from one version to version PostgreSQL supports the +mechanism of migration (upgrade & downgrade) files. These are files named like +`extname--0.0.1--0.0.2.sql` upgrading from version `0.0.1` to `0.0.2` and +`extname--0.0.2--0.0.1.sql` downgrading the other way. - $ dumbo migrations +Maintaining the changes between versions in these files manually is tedious and +error-prone and Dumbo does it for you automatically. -### Using ERB templates + $ dumbo migrations -TODO: illustrate this! +Note that Dumbo differentiates between extension versions by looking for files structured +`extname--major.minor.patch.sql`. Here `extname` is the name of the extension +and the `major.minor.patch` is the version - e.g. `0.1.1`. ![](http://img1.wikia.nocookie.net/__cb20091210033559/disney/images/7/76/Dumbo-HQ.JPG) diff --git a/bin/dumbo b/bin/dumbo index 12c518a..706b318 100755 --- a/bin/dumbo +++ b/bin/dumbo @@ -1,6 +1,57 @@ #!/usr/bin/env ruby -# require 'dumbo' -require File.join(File.dirname(__FILE__), '..', 'lib', 'dumbo.rb') +require 'dumbo' + +module Dumbo + class Cli < Thor + desc 'new [initial_version] [extension_comment]', 'Create a new PostgreSQL extension skeleton' + def new(name, initial_version = '0.0.1', extension_comment = 'My awesome extension') + # TODO validate initial_version if passed + + Dumbo::Command::New.exec(name, initial_version, extension_comment, &print) + say("Now building the extension SQL file.") + Dir.chdir(name) + Dumbo::Command::Build.exec(&print) + Dir.chdir('..') + end + + desc 'build', 'Concatinate SQL files into the extension\' SQL file in format `extname--1.0.1.sql`' + def build + Dumbo::Command::Build.exec(&print) + end + + desc 'bump [major|minor|patch]', 'Bump the version level on the extension\'s extname.control file' + def bump(level = 'patch') + level = level.downcase + + error unless ['major', 'minor', 'patch'].include?(level) + + Dumbo::Command::Bump.exec(level, &print) + + say("Updated #{Extension.control_file} to version #{Extension.version}") + end + + desc 'migrations', 'Compare the last two versions of the extension and build migration files' + def migrations + if !Dumbo.boot('development') + return + end + + Dumbo::Command::Migrations.exec(&print) + end + + no_commands do + def print + Proc.new do |status, path, colour| + if colour.nil? + say_status(status, path) + else + say_status(status, path, colour) + end + end + end + end + end +end Dumbo::Cli.start(ARGV) diff --git a/dumbo.gemspec b/dumbo.gemspec index 853306b..6c6f0e0 100644 --- a/dumbo.gemspec +++ b/dumbo.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| spec.version = Dumbo::VERSION spec.authors = ['Manuel Kniep'] spec.email = ['m.kniep@web.de'] - spec.summary = %q{postgres extension with fun} + spec.summary = %q{PostgreSQL extensions with fun} spec.homepage = 'https://github.com/adjust/dumbo' spec.license = 'MIT' @@ -18,6 +18,8 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'erubis', '~> 2.7' - spec.add_dependency 'pg', '~> 0.17' + spec.add_dependency 'pg', '~> 0.19' spec.add_dependency 'thor', '~> 0.19' + + spec.required_ruby_version = '>= 2.0.0' end diff --git a/lib/dumbo.rb b/lib/dumbo.rb index 473d79e..08920b7 100644 --- a/lib/dumbo.rb +++ b/lib/dumbo.rb @@ -1,22 +1,13 @@ -require 'dumbo/version' -require 'dumbo/db' -require 'dumbo/pg_object' -require 'dumbo/cli' -require 'dumbo/type' -require 'dumbo/types/composite_type' -require 'dumbo/types/enum_type' -require 'dumbo/types/range_type' -require 'dumbo/types/base_type' -require 'dumbo/function' -require 'dumbo/cast' -require 'dumbo/aggregate' -require 'dumbo/dependency_resolver' -require 'dumbo/extension' -require 'dumbo/extension_migrator' -require 'dumbo/extension_version' -require 'dumbo/operator' -require 'dumbo/version' -require 'dumbo/binding_loader' +require 'thor' +require 'erubis' +require 'fileutils' +require 'pathname' + +['', 'command', 'pg_object', 'pg_object/type'].each do |submodule| + Dir.glob("#{File.dirname(__FILE__)}/dumbo/#{submodule}/*.rb").each do |path| + require File.expand_path(path) + end +end module Dumbo class NoConfigurationError < StandardError @@ -26,14 +17,45 @@ def initialize(env) end class << self + def extension_file(*files) + File.join(extension_root, *files) + end + + def extension_files(*files) + Dir.glob(extension_file(*files)) + end + + def template_root + File.join(File.dirname(__FILE__), '..', 'template') + end + + def template_files(*files) + Dir.glob(File.join(template_root, *files)) + end + def boot(env) raise NoConfigurationError.new(env) if db_config[env].nil? - DB.connect db_config[env] + if !DB.connect(db_config[env]) + $stderr.puts("Error connecting to PostgreSQL using connection string: `#{connstring(env)}`.") + return false + end + + true end def db_config @config ||= YAML.load_file(File.join('config', 'database.yml')) end + + private + + def connstring(env) + db_config[env].map { |key, value| "#{key}=#{value}" }.join(' ') + end + + def extension_root + FileUtils.pwd + end end end diff --git a/lib/dumbo/aggregate.rb b/lib/dumbo/aggregate.rb deleted file mode 100644 index 704d235..0000000 --- a/lib/dumbo/aggregate.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Dumbo - class Aggregate < PgObject - attr_accessor :name, :sfunc, :transname, :ffunc, :input_data_type, :state_data_type, - :initial_condition, :sort_operator - identfied_by :name, :input_data_type - - def load_attributes - result = DB.exec <<-SQL - SELECT - proname AS name, - pg_get_function_arguments(pr.oid) AS input_data_type, - aggtransfn AS sfunc, - aggfinalfn AS ffunc, - agginitval AS initial_condition, - op.oprname AS sort_operator, - proargtypes, - aggtranstype AS state_data_type , proacl, - CASE WHEN (tt.typlen = -1 AND tt.typelem != 0) THEN (SELECT at.typname FROM pg_type at WHERE at.oid = tt.typelem) || '[]' ELSE tt.typname END as state_data_type, - prorettype AS aggfinaltype, - --CASE WHEN (tf.typlen = -1 AND tf.typelem != 0) THEN (SELECT at.typname FROM pg_type at WHERE at.oid = tf.typelem) || '[]' ELSE tf.typname END as ffunc, - description, - (SELECT array_agg(label) FROM pg_seclabels sl1 WHERE sl1.objoid=aggfnoid) AS labels, - (SELECT array_agg(provider) FROM pg_seclabels sl2 WHERE sl2.objoid=aggfnoid) AS providers, oprname, opn.nspname as oprnsp - FROM pg_aggregate ag - LEFT OUTER JOIN pg_operator op ON op.oid=aggsortop - LEFT OUTER JOIN pg_namespace opn ON opn.oid=op.oprnamespace - JOIN pg_proc pr ON pr.oid = ag.aggfnoid - JOIN pg_type tt on tt.oid=aggtranstype - JOIN pg_type tf on tf.oid=prorettype - LEFT OUTER JOIN pg_description des ON (des.objoid=aggfnoid::oid AND des.classoid='pg_aggregate'::regclass) - WHERE aggfnoid = #{oid} - SQL - - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - - result.first - end - - def to_sql - attributes = [] - attributes << "SFUNC = #{sfunc}" - attributes << "STYPE = #{state_data_type}" - attributes << "FINALFUNC = #{ffunc}" if ffunc && ffunc != '-' - attributes << "INITCOND = '#{initial_condition}'" if initial_condition - attributes << "SORTOP = \"#{sort_operator}\"" if sort_operator - - <<-SQL.gsub(/^ {6}/, '') - CREATE AGGREGATE #{name}(#{input_data_type}) ( - #{attributes.join(",\n ")} - ); - SQL - end - - def drop - "DROP AGGREGATE IF EXISTS #{name} (#{input_data_type});" - end - end -end diff --git a/lib/dumbo/binding_loader.rb b/lib/dumbo/binding_loader.rb index ced1526..9b68505 100644 --- a/lib/dumbo/binding_loader.rb +++ b/lib/dumbo/binding_loader.rb @@ -19,7 +19,7 @@ def load def load_list files = [] - IO.foreach(@file) do |line| + IO.foreach(file) do |line| catch(:done) do load_file = parse(line) files << load_file if load_file diff --git a/lib/dumbo/cast.rb b/lib/dumbo/cast.rb deleted file mode 100644 index 5dfe5c6..0000000 --- a/lib/dumbo/cast.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Dumbo - class Cast < PgObject - attr_accessor :source_type, :target_type, :function_name, :argument_type, :context - identfied_by :source_type, :target_type - - def load_attributes - result = DB.exec <<-SQL - SELECT - format_type(st.oid,NULL) AS source_type, - format_type(st.oid,NULL) AS argument_type, - format_type(tt.oid,tt.typtypmod) AS target_type, - proname AS function_name, - CASE WHEN ca.castcontext = 'e' THEN NULL - WHEN ca.castcontext = 'a' THEN 'ASSIGNMENT' - ELSE 'IMPLICIT' - END AS context - - FROM pg_cast ca - JOIN pg_type st ON st.oid=castsource - JOIN pg_type tt ON tt.oid=casttarget - LEFT JOIN pg_proc pr ON pr.oid=castfunc - LEFT OUTER JOIN pg_description des ON (des.objoid=ca.oid AND des.objsubid=0 AND des.classoid='pg_cast'::regclass) - WHERE ca.oid = #{oid} - SQL - - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - - result.first - end - - def drop - "DROP CAST IF EXISTS (#{source_type} AS #{target_type});" - end - - def to_sql - attributes = [] - attributes << "WITH FUNCTION #{function_name}(#{source_type})" if function_name - attributes << 'WITHOUT FUNCTION' unless function_name - attributes << context if context - - <<-SQL.gsub(/^ {6}/, '') - CREATE CAST (#{source_type} AS #{target_type}) - #{attributes.join("\nAS ")}; - SQL - end - end -end diff --git a/lib/dumbo/cli.rb b/lib/dumbo/cli.rb deleted file mode 100644 index 655921e..0000000 --- a/lib/dumbo/cli.rb +++ /dev/null @@ -1,158 +0,0 @@ -require 'thor' -require 'thor/group' -require 'erubis' -require 'fileutils' -require 'pathname' - -module Dumbo - class Cli < Thor - class InvalidVersionLevel < StandardError - def initialize - super 'Argument must be major, minor or patch' - end - end - - desc 'migrations', 'Compare the last two versions of the extension and build migration files' - def migrations - Dumbo.boot('development') - - old_version, new_version = Extension.versions.last(2).map(&:to_s) - - if new_version - ExtensionMigrator.new(Extension.name, old_version, new_version).create - end - end - - desc 'build', 'Concatinate SQL files and build the extension file in format `extname--1.0.1.sql`' - def build - file_list = DependencyResolver.new(Dir.glob('sql/**/*.{sql,erb}')).resolve - - file_list.each do |t| - sql = t.prerequisites.map do |file| - ["--source file #{file}"] + get_sql(file) + [' '] - end.flatten - - concatenate(sql, Extension.file_name) - end - - Dir.glob('src/**/*.erb').each do |file| - src = convert_template(file) - out = Pathname.new(file).sub_ext('') - File.open(out.to_s, 'w') do |f| - f.puts(src.join("\n")) - end - end - end - - desc 'bump [major|minor|patch]', 'Bump the version level on an existing .control file' - def bump(level = 'patch') - level = level.downcase - - raise InvalidVersionLevel unless ['major', 'minor', 'patch'].include?(level) - - version = ExtensionVersion.new_from_string(Extension.version).bump(level).to_s - - Extension.version!(version) - end - - desc 'new ', 'Create a new PostgreSQL extension skeleton' - def new(name, initial_version = '0.0.1', extension_comment = 'My awesome extension') - template_path = File.join(File.dirname(__FILE__), '..', '..', 'template') - - name = name.gsub('-', '_') - - Dir.glob(File.join(template_path, '**', '*')).each do |template| - pathname = Pathname.new(template) - dest_name = pathname.relative_path_from Pathname.new(template_path) - - if pathname.directory? - mkdir("#{name}/#{dest_name}", true) - next - end - - names_map = { - 'sample.control.erb' => "#{name}.control", - 'sql/sample.sql.erb' => "sql/#{name}.sql", - 'src/sample.c.erb' => "src/#{name}.c", - 'src/sample.h.erb' => "src/#{name}.h", - 'test/expected/sample_test.out.erb' => "test/expected/#{name}_test.out", - 'test/sql/sample_test.sql.erb' => "test/sql/#{name}_test.sql" - } - - if dest_name.extname == '.erb' - eruby = Erubis::Eruby.new(File.read(template)) - - erb_mapping = { ext_name: name, extension_comment: extension_comment, initial_version: initial_version } - - content = eruby.result(erb_mapping) - create "#{name}/#{(names_map[dest_name.to_s] || dest_name.sub_ext(''))}", content - else - cp template, "#{name}/#{(names_map[dest_name.to_s] || dest_name)}" - end - end - end - - no_commands do - def mkdir(path, silent_skip = false) - if File.directory?(path) - say_status('skip', "#{path}", :yellow) unless silent_skip - else - FileUtils.mkdir_p(path) - say_status('create', "#{path}") - end - end - - def create(path, content, silent_skip = false) - if File.exist? path - say_status('skip', path, :yellow) unless silent_skip - else - File.open(path, 'w') do |f| - f.puts content - end - say_status('create', path) - end - end - - def cp(src, dest, silent_skip = false) - Array(src).each do |p| - path = Pathname.new(p) - if File.directory?(dest) - if File.exist?("#{dest}/#{path.basename}") - say_status('skip', "#{dest}/#{path.basename}", :yellow) unless silent_skip - else - FileUtils.cp_r p, dest - say_status('create', "#{dest}/#{path.basename}") - end - else - if File.exist?(dest) - say_status('skip', dest, :yellow) unless silent_skip - else - FileUtils.cp_r p, dest - say_status('create', dest) - end - end - end - end - - def concatenate(lines, target) - File.open(target, 'w') do |f| - f.puts "-- complain if script is sourced in psql, rather than via CREATE EXTENSION" - f.puts "\\echo Use \"CREATE EXTENSION #{Extension.name}\" to load this file. \\quit" - lines.each do |line| - f.puts line unless line =~ DependencyResolver.depends_pattern - end - end - end - - def convert_template(file) - eruby = Erubis::Eruby.new(File.read(file)) - bindigs = get_bindings(file) - eruby.result(bindigs).split("\n") - end - - def get_bindings(file) - BindingLoader.new(file).load - end - end - end -end diff --git a/lib/dumbo/command.rb b/lib/dumbo/command.rb new file mode 100644 index 0000000..0f7879d --- /dev/null +++ b/lib/dumbo/command.rb @@ -0,0 +1,9 @@ +module Dumbo + module Command + class Base + def self.exec(*params, &block) + new(*params).exec(&block) + end + end + end +end diff --git a/lib/dumbo/command/build.rb b/lib/dumbo/command/build.rb new file mode 100644 index 0000000..ee0eccb --- /dev/null +++ b/lib/dumbo/command/build.rb @@ -0,0 +1,56 @@ +module Dumbo + module Command + class Build < Dumbo::Command::Base + def exec(&block) + file_list = DependencyResolver.new(Dumbo.extension_files('sql', '**', '*.{sql,erb}')).resolve + + lines = file_list.map do |file| + ["--source file #{file}"] + get_sql(file) + [' '] + end + + concatenate(lines, Extension.file_name, &block) + + Dumbo.extension_files('src', '**', '*.erb').each do |file| + src = convert_template(file) + out = Pathname.new(file).sub_ext('') + File.open(out.to_s, 'w') do |f| + f.puts(src.join("\n")) + end + end + end + + private + + def concatenate(lines, target, &block) + File.open(target, 'w') do |f| + f.puts "-- complain if script is sourced in psql, rather than via CREATE EXTENSION" + f.puts "\\echo Use \"CREATE EXTENSION #{Extension.name}\" to load this file. \\quit" + lines.each do |line| + f.puts line unless line =~ DependencyResolver.depends_pattern + end + end + + yield('created', target) if block_given? + end + + def convert_template(file) + eruby = Erubis::Eruby.new(File.read(file)) + bindigs = get_bindings(file) + eruby.result(bindigs).split("\n") + end + + def get_bindings(file) + BindingLoader.new(file).load + end + + def get_sql(file) + ext = Pathname.new(file).extname + if ext == '.erb' + convert_template(file) + else + File.readlines(file) + end + end + end + end +end diff --git a/lib/dumbo/command/bump.rb b/lib/dumbo/command/bump.rb new file mode 100644 index 0000000..b58c3c8 --- /dev/null +++ b/lib/dumbo/command/bump.rb @@ -0,0 +1,15 @@ +module Dumbo + module Command + class Bump < Dumbo::Command::Base + attr_accessor :level + + def initialize(level) + @level = level + end + + def exec + Extension.version!(ExtensionVersion.new_from_string(Extension.version).bump(level).to_s) + end + end + end +end diff --git a/lib/dumbo/command/migrations.rb b/lib/dumbo/command/migrations.rb new file mode 100644 index 0000000..f839817 --- /dev/null +++ b/lib/dumbo/command/migrations.rb @@ -0,0 +1,13 @@ +module Dumbo + module Command + class Migrations < Dumbo::Command::Base + def exec + old_version, new_version = Extension.versions.last(2).map(&:to_s) + + if new_version + ExtensionMigrator.new(Extension.name, old_version, new_version).create + end + end + end + end +end diff --git a/lib/dumbo/command/new.rb b/lib/dumbo/command/new.rb new file mode 100644 index 0000000..c9929e7 --- /dev/null +++ b/lib/dumbo/command/new.rb @@ -0,0 +1,104 @@ +module Dumbo + module Command + class New < Dumbo::Command::Base + attr_accessor :name, :initial_version, :extension_comment + + def initialize(name, initial_version, extension_comment) + @name = name + @initial_version = initial_version + @extension_comment = extension_comment + end + + def exec(&block) + mkdir(name) + + Dumbo.template_files('**', '*').each do |template| + pathname = Pathname.new(template) + + dest_name = pathname.relative_path_from(Pathname.new(Dumbo.template_root)) + + if pathname.directory? + mkdir(Dumbo.extension_file(name, dest_name), &block) + next + end + + if dest_name.extname == '.erb' + eruby = Erubis::Eruby.new(File.read(template)) + + content = eruby.result(erb_mapping) + file = Dumbo.extension_file(name, names_map[dest_name.to_s] || dest_name.sub_ext('')) + create(file, content, &block) + else + cp(template, Dumbo.extension_file(name, names_map[dest_name.to_s] || dest_name), &block) + end + end + end + + def name + @name.gsub('-', '_') + end + + private + + def names_map + { + 'sample.control.erb' => "#{name}.control", + 'sql/sample.sql.erb' => "sql/#{name}.sql", + 'src/sample.c.erb' => "src/#{name}.c", + 'src/sample.h.erb' => "src/#{name}.h", + 'test/expected/sample_test.out.erb' => "test/expected/#{name}_test.out", + 'test/sql/sample_test.sql.erb' => "test/sql/#{name}_test.sql" + } + end + + def erb_mapping + { + ext_name: name, + extension_comment: extension_comment, + initial_version: initial_version + } + end + + def mkdir(path) + if File.directory?(path) + yield('skip', "#{path}", :yellow) if block_given? + else + FileUtils.mkdir_p(path) + yield('create', "#{path}") if block_given? + end + end + + def create(path, content, &block) + if File.exist? path + yield('skip', path, :yellow) if block_given? + else + File.open(path, 'w') do |f| + f.puts content + end + yield('create', path) if block_given? + end + end + + def cp(src, dest, &block) + Array(src).each do |p| + path = Pathname.new(p) + if File.directory?(dest) + if File.exist?("#{dest}/#{path.basename}") + yield('skip', "#{dest}/#{path.basename}", :yellow) if block_given? + else + FileUtils.cp_r(p, dest) + yield('create', "#{dest}/#{path.basename}") if block_given? + end + else + if File.exist?(dest) + yield('skip', dest, :yellow) if block_given? + else + FileUtils.cp_r p, dest + yield('create', dest) if block_given? + end + end + end + end + end + end +end diff --git a/lib/dumbo/db.rb b/lib/dumbo/db.rb index 9361e3a..2ea7d2d 100644 --- a/lib/dumbo/db.rb +++ b/lib/dumbo/db.rb @@ -15,6 +15,8 @@ def initialize class << self def connect(config) @_connection ||= PG.connect(config) + rescue PG::ConnectionBad => e + false end def connection @@ -25,6 +27,9 @@ def connection def exec(sql) connection.exec(sql) + rescue PG::Error => e + $stderr.puts("Error executing SQL:\n\n#{sql}\n\n#{e.inspect}") + Kernel.exit 1 end def transaction(&block) @@ -33,33 +38,6 @@ def transaction(&block) rescue DB::Rollback end end - - # TODO this is likely obsolete - consider removing - # - # def structure_load(config, filename) - # ENV['PGHOST'] = config['host'] if config['host'] - # ENV['PGPORT'] = config['port'].to_s if config['port'] - # ENV['PGPASSWORD'] = config['password'].to_s if config['password'] - # ENV['PGUSER'] = config['username'].to_s if config['username'] - - # args = ['-v', 'ON_ERROR_STOP=1', '-q', '-f', filename] - # args << config['dbname'] - - # run_cmd('psql', args) - # end - - # private - - # def run_cmd(cmd, args) - # fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args) - # end - - # def run_cmd_error(cmd, args) - # msg = "failed to execute:\n" - # msg << "#{cmd} #{args.join(' ')}\n\n" - # msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" - # msg - # end end end end diff --git a/lib/dumbo/extension.rb b/lib/dumbo/extension.rb index 620dc11..04f7101 100644 --- a/lib/dumbo/extension.rb +++ b/lib/dumbo/extension.rb @@ -2,14 +2,14 @@ module Dumbo class Extension < Struct.new(:name, :version) class << self def name - @_name ||= File.read(makefile)[/EXTENSION\s*=\s*([^\s]*)/, 1] + File.read(makefile)[/EXTENSION\s*=\s*([^\s]*)/, 1] rescue SystemCallError => e STDERR.puts("File not found: #{makefile}") raise e end def version - @_version ||= File.read(control_file)[/default_version\s*=\s*'([^']*)'/, 1] + File.read(control_file)[/default_version\s*=\s*'([^']*)'/, 1] rescue SystemCallError => e STDERR.puts("File not found: #{control_file}") raise e @@ -21,22 +21,26 @@ def versions def version!(new_version) content = File.read(control_file) - new_content = content.gsub(version, new_version) + new_content = content.gsub(/\n\s*default_version\s*=.*[\n\Z]/, "\ndefault_version = '#{new_version}'\n") File.open(control_file, 'w') { |file| file.puts new_content } end def file_name - "#{name}--#{version}.sql" + Dumbo.extension_file("#{name}--#{version}.sql") end - private - def makefile - 'Makefile' + Dumbo.extension_file('Makefile') end def control_file - "#{name}.control" + Dumbo.extension_file("#{name}.control") + end + + def make_install + return unless File.exists?(makefile) + + Kernel.system('make install > /dev/null') end end @@ -50,7 +54,7 @@ def version # main releases without migrations def releases - Dir.glob("#{name}--*.sql").reject { |f| f =~ /\d--\d/ } + Dumbo.extension_files("#{name}--*.sql").reject { |file| file =~ /\d--\d/ } end def versions @@ -66,6 +70,8 @@ def versions def create DB.exec "DROP EXTENSION IF EXISTS #{name}" + Extension.make_install + create_sql = "CREATE EXTENSION #{name}" create_sql = "#{create_sql} VERSION '#{version}'" unless version.nil? @@ -94,28 +100,28 @@ def objects ORDER BY 1; SQL - result.map { |r| PgObject.new(r['objid']).get(r['classid']) } + result.map { |r| PgObject::Base.new(r['objid']).get(r['classid']) } end end def types - objects.select { |o| o.kind_of?(Type) } + objects.select { |o| o.kind_of?(PgObject::Type::Base) } end def functions - objects.select { |o| o.kind_of?(Function) } + objects.select { |o| o.kind_of?(PgObject::Function) } end def casts - objects.select { |o| o.kind_of?(Cast) } + objects.select { |o| o.kind_of?(PgObject::Cast) } end def operators - objects.select { |o| o.kind_of?(Operator) } + objects.select { |o| o.kind_of?(PgObject::Operator) } end def aggregates - objects.select { |o| o.kind_of?(Aggregate) } + objects.select { |o| o.kind_of?(PgObject::Aggregate) } end end end diff --git a/lib/dumbo/extension_migrator.rb b/lib/dumbo/extension_migrator.rb index 9932390..5dde042 100644 --- a/lib/dumbo/extension_migrator.rb +++ b/lib/dumbo/extension_migrator.rb @@ -2,7 +2,7 @@ module Dumbo class ExtensionMigrator attr_reader :old_version, :new_version, :name - TYPES = [:types, :functions, :casts, :operators, :aggregates] + PG_OBJECTS = [:types, :functions, :casts, :operators, :aggregates] def initialize(name, old_version, new_version) @name = name @@ -22,14 +22,14 @@ def create end def upgrade - TYPES.map do |type| + PG_OBJECTS.map do |type| diff = object_diff(type, :upgrade) "----#{type}----\n" + diff unless diff.empty? end.compact.join("\n") end def downgrade - TYPES.reverse.map do |type| + PG_OBJECTS.reverse.map do |type| diff = object_diff(type, :downgrade) "----#{type}----\n" + diff unless diff.empty? end.compact.join("\n") @@ -43,12 +43,12 @@ def cast_diff object_diff(@old_version.casts, @new_version.casts) end - def object_diff(type, dir) + def object_diff(type, direction) ids = @old_version.public_send(type).map(&:identify) | @new_version.public_send(type).map(&:identify) sqls = ids.map do |id| new_version_obj = @new_version.public_send(type).find { |n| n.identify == id } old_version_obj = @old_version.public_send(type).find { |n| n.identify == id } - case dir + case direction when :upgrade migrate(old_version_obj, new_version_obj) when :downgrade diff --git a/lib/dumbo/extension_version.rb b/lib/dumbo/extension_version.rb index 4048af2..16d013f 100644 --- a/lib/dumbo/extension_version.rb +++ b/lib/dumbo/extension_version.rb @@ -2,14 +2,8 @@ module Dumbo class ExtensionVersion < Struct.new(:major, :minor, :patch) include Comparable - class << self - def new_from_string(version) - ExtensionVersion.new(*version.split('.').map(&:to_i)) - end - - def sort - sort! { |a, b| a <=> b } - end + def self.new_from_string(version) + ExtensionVersion.new(*version.split('.').map(&:to_i)) end def <=>(other) diff --git a/lib/dumbo/function.rb b/lib/dumbo/function.rb deleted file mode 100644 index 4d8045a..0000000 --- a/lib/dumbo/function.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Dumbo - class Function < PgObject - attr_accessor :name, :result_type, :definition, :type, :arg_types - identfied_by :name, :arg_types - - def initialize(oid) - super - get - end - - def get - if type == 'agg' - Aggregate.new(oid) - else - self - end - end - - def drop - "DROP FUNCTION IF EXISTS #{name}(#{arg_types});" - end - - def migrate_to(other) - - if other.identify != identify - fail 'Not the Same Objects!' - end - - return nil if other.to_sql == to_sql - - if other.result_type != result_type - <<-SQL.gsub(/^ {8}/, '') - #{drop} - #{other.to_sql} - SQL - else - other.to_sql - end - end - - def load_attributes - result = DB.exec <<-SQL - SELECT - p.proname as name, - pg_catalog.pg_get_function_result(p.oid) as result_type, - pg_catalog.pg_get_function_arguments(p.oid) as args, - pg_catalog.pg_get_function_identity_arguments(p.oid) as arg_types, - CASE - WHEN p.proisagg THEN 'agg' - WHEN p.proiswindow THEN 'window' - WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger' - ELSE 'normal' - END as "type", - CASE - WHEN p.provolatile = 'i' THEN 'immutable' - WHEN p.provolatile = 's' THEN 'stable' - WHEN p.provolatile = 'v' THEN 'volatile' - END as volatility, - proisstrict as is_strict, - l.lanname as language, - p.prosrc as "source", - pg_catalog.obj_description(p.oid, 'pg_proc') as description, - CASE WHEN p.proisagg THEN 'agg_dummy' ELSE pg_get_functiondef(p.oid) END as definition - - FROM pg_catalog.pg_proc p - LEFT JOIN pg_catalog.pg_language l ON l.oid = p.prolang - WHERE pg_catalog.pg_function_is_visible(p.oid) - AND p.oid = #{oid}; - SQL - - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - - result.first - end - - def to_sql - definition.gsub("public.#{name}", name).strip + ';' - end - end -end diff --git a/lib/dumbo/operator.rb b/lib/dumbo/operator.rb deleted file mode 100644 index 26c0b07..0000000 --- a/lib/dumbo/operator.rb +++ /dev/null @@ -1,72 +0,0 @@ -module Dumbo - class Operator < PgObject - attr_accessor :name, - :kind, - :hashes, - :merges, - :leftarg, - :rightarg, - :result_type, - :commutator, - :negator, - :function_name, - :join, - :restrict - - identfied_by :name, :leftarg, :rightarg - - def load_attributes - result = DB.exec <<-SQL - SELECT - op.oprname AS name, - op.oprkind AS kind, - op.oprcanhash AS hashes, - op.oprcanmerge AS merges, - lt.typname AS leftarg, - rt.typname AS rightarg, - et.typname AS result_type, - co.oprname AS commutator, - ne.oprname AS negator, - op.oprcode AS function_name, - op.oprjoin AS join, - op.oprrest AS restrict, - description - FROM pg_operator op - LEFT OUTER JOIN pg_type lt ON lt.oid=op.oprleft - LEFT OUTER JOIN pg_type rt ON rt.oid=op.oprright - JOIN pg_type et on et.oid=op.oprresult - LEFT OUTER JOIN pg_operator co ON co.oid=op.oprcom - LEFT OUTER JOIN pg_operator ne ON ne.oid=op.oprnegate - LEFT OUTER JOIN pg_description des ON des.objoid=op.oid - WHERE op.oid = #{oid} - SQL - - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - - result.first - end - - def to_sql - attrs = [:leftarg, :rightarg, :commutator, :negator, :restrict, :join].reduce([]) do |mem, attr| - mem << "#{attr.to_s.upcase} = #{public_send(attr)}" if public_send(attr) && public_send(attr) !='-' - mem - end - - attrs << "HASHES" if hashes == 't' - attrs << "MERGES" if merges == 't' - atttr_str = attrs.join(",\n ") - <<-SQL.gsub(/^ {6}/, '') - CREATE OPERATOR #{name} ( - PROCEDURE = #{function_name}, - #{atttr_str} - ); - SQL - end - - def drop - "DROP OPERATOR IF EXISTS #{name} (#{leftarg}, #{rightarg});" - end - end -end diff --git a/lib/dumbo/pg_object.rb b/lib/dumbo/pg_object.rb index 1471352..b38e085 100644 --- a/lib/dumbo/pg_object.rb +++ b/lib/dumbo/pg_object.rb @@ -1,85 +1,87 @@ require 'singleton' module Dumbo - class PgObject - class Unregistered < StandardError - def initialize - super 'PgObject classes must declare `identified_by` parameters' + module PgObject + class Base + class Unregistered < StandardError + def initialize + super 'PgObject classes must declare `identified_by` parameters' + end end - end - class Registry - include ::Singleton + class Registry + include ::Singleton - attr_reader :identifiers + attr_reader :identifiers - class << self - def identifiers - instance.identifiers - end + class << self + def identifiers + instance.identifiers + end - def identifier(klass) - klass.ancestors.each do |ancestor| - identifier = instance.identifiers[ancestor] + def identifier(klass) + klass.ancestors.each do |ancestor| + identifier = instance.identifiers[ancestor] - return identifier unless identifier.nil? - end + return identifier unless identifier.nil? + end - raise PgObject::Unregistered + raise PgObject::Unregistered + end end - end - def identifiers - @identifiers ||= {} + def identifiers + @identifiers ||= {} + end end - end - attr_reader :oid + attr_reader :oid - def self.identfied_by(*args) - Registry.identifiers[self] = args - end + def self.identfied_by(*args) + Registry.identifiers[self] = args + end - def initialize(oid) - @oid = oid - load_attributes - end + def initialize(oid) + @oid = oid + load_attributes + end - def identifier - Registry.identifier(self.class) - end + def identifier + Registry.identifier(self.class) + end - def identify - identifier.map { |a| public_send a } - end + def identify + identifier.map { |a| public_send a } + end - def get(type = nil) - case type - when 'function', 'pg_proc' - Function.new(oid).get - when 'cast', 'pg_cast' - Cast.new(oid).get - when 'operator', 'pg_operator' - Operator.new(oid).get - when 'type', 'pg_type' - Type.new(oid).get - else - load_attributes - self + def get(type = nil) + case type + when 'function', 'pg_proc' + PgObject::Function.new(oid).get + when 'cast', 'pg_cast' + PgObject::Cast.new(oid).get + when 'operator', 'pg_operator' + PgObject::Operator.new(oid).get + when 'type', 'pg_type' + PgObject::Type::Base.new(oid).get + else + load_attributes + self + end end - end - def load_attributes - end + def load_attributes + end - def migrate_to(other) - fail 'Not the Same Objects!' unless other.identify == identify + def migrate_to(other) + fail 'Not the Same Objects!' unless other.identify == identify - if other.to_sql != to_sql - <<-SQL.gsub(/^ {8}/, '') - #{drop} - #{other.to_sql} - SQL + if other.to_sql != to_sql + <<-SQL.gsub(/^ {8}/, '') + #{drop} + #{other.to_sql} + SQL + end end end end diff --git a/lib/dumbo/pg_object/aggregate.rb b/lib/dumbo/pg_object/aggregate.rb new file mode 100644 index 0000000..9c8d055 --- /dev/null +++ b/lib/dumbo/pg_object/aggregate.rb @@ -0,0 +1,62 @@ +module Dumbo + module PgObject + class Aggregate < Dumbo::PgObject::Base + attr_accessor :name, :sfunc, :transname, :ffunc, :input_data_type, :state_data_type, + :initial_condition, :sort_operator + identfied_by :name, :input_data_type + + def load_attributes + result = DB.exec <<-SQL + SELECT + proname AS name, + pg_get_function_arguments(pr.oid) AS input_data_type, + aggtransfn AS sfunc, + aggfinalfn AS ffunc, + agginitval AS initial_condition, + op.oprname AS sort_operator, + proargtypes, + aggtranstype AS state_data_type , proacl, + CASE WHEN (tt.typlen = -1 AND tt.typelem != 0) THEN (SELECT at.typname FROM pg_type at WHERE at.oid = tt.typelem) || '[]' ELSE tt.typname END as state_data_type, + prorettype AS aggfinaltype, + --CASE WHEN (tf.typlen = -1 AND tf.typelem != 0) THEN (SELECT at.typname FROM pg_type at WHERE at.oid = tf.typelem) || '[]' ELSE tf.typname END as ffunc, + description, + (SELECT array_agg(label) FROM pg_seclabels sl1 WHERE sl1.objoid=aggfnoid) AS labels, + (SELECT array_agg(provider) FROM pg_seclabels sl2 WHERE sl2.objoid=aggfnoid) AS providers, oprname, opn.nspname as oprnsp + FROM pg_aggregate ag + LEFT OUTER JOIN pg_operator op ON op.oid=aggsortop + LEFT OUTER JOIN pg_namespace opn ON opn.oid=op.oprnamespace + JOIN pg_proc pr ON pr.oid = ag.aggfnoid + JOIN pg_type tt on tt.oid=aggtranstype + JOIN pg_type tf on tf.oid=prorettype + LEFT OUTER JOIN pg_description des ON (des.objoid=aggfnoid::oid AND des.classoid='pg_aggregate'::regclass) + WHERE aggfnoid = #{oid} + SQL + + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + + result.first + end + + def to_sql + attributes = [] + attributes << "SFUNC = #{sfunc}" + attributes << "STYPE = #{state_data_type}" + attributes << "FINALFUNC = #{ffunc}" if ffunc && ffunc != '-' + attributes << "INITCOND = '#{initial_condition}'" if initial_condition + attributes << "SORTOP = \"#{sort_operator}\"" if sort_operator + + <<-SQL.gsub(/^ {6}/, '') + CREATE AGGREGATE #{name}(#{input_data_type}) ( + #{attributes.join(",\n ")} + ); + SQL + end + + def drop + "DROP AGGREGATE IF EXISTS #{name} (#{input_data_type});" + end + end + end +end diff --git a/lib/dumbo/pg_object/cast.rb b/lib/dumbo/pg_object/cast.rb new file mode 100644 index 0000000..71fc845 --- /dev/null +++ b/lib/dumbo/pg_object/cast.rb @@ -0,0 +1,51 @@ +module Dumbo + module PgObject + class Cast < Dumbo::PgObject::Base + attr_accessor :source_type, :target_type, :function_name, :argument_type, :context + identfied_by :source_type, :target_type + + def load_attributes + result = DB.exec <<-SQL + SELECT + format_type(st.oid,NULL) AS source_type, + format_type(st.oid,NULL) AS argument_type, + format_type(tt.oid,tt.typtypmod) AS target_type, + proname AS function_name, + CASE WHEN ca.castcontext = 'e' THEN NULL + WHEN ca.castcontext = 'a' THEN 'ASSIGNMENT' + ELSE 'IMPLICIT' + END AS context + + FROM pg_cast ca + JOIN pg_type st ON st.oid=castsource + JOIN pg_type tt ON tt.oid=casttarget + LEFT JOIN pg_proc pr ON pr.oid=castfunc + LEFT OUTER JOIN pg_description des ON (des.objoid=ca.oid AND des.objsubid=0 AND des.classoid='pg_cast'::regclass) + WHERE ca.oid = #{oid} + SQL + + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + + result.first + end + + def drop + "DROP CAST IF EXISTS (#{source_type} AS #{target_type});" + end + + def to_sql + attributes = [] + attributes << "WITH FUNCTION #{function_name}(#{source_type})" if function_name + attributes << 'WITHOUT FUNCTION' unless function_name + attributes << context if context + + <<-SQL.gsub(/^ {6}/, '') + CREATE CAST (#{source_type} AS #{target_type}) + #{attributes.join("\nAS ")}; + SQL + end + end + end +end diff --git a/lib/dumbo/pg_object/function.rb b/lib/dumbo/pg_object/function.rb new file mode 100644 index 0000000..75802be --- /dev/null +++ b/lib/dumbo/pg_object/function.rb @@ -0,0 +1,84 @@ +module Dumbo + module PgObject + class Function < Dumbo::PgObject::Base + attr_accessor :name, :result_type, :definition, :type, :arg_types + identfied_by :name, :arg_types + + def initialize(oid) + super + get + end + + def get + if type == 'agg' + Aggregate.new(oid) + else + self + end + end + + def drop + "DROP FUNCTION IF EXISTS #{name}(#{arg_types});" + end + + def migrate_to(other) + + if other.identify != identify + fail 'Not the Same Objects!' + end + + return nil if other.to_sql == to_sql + + if other.result_type != result_type + <<-SQL.gsub(/^ {8}/, '') + #{drop} + #{other.to_sql} + SQL + else + other.to_sql + end + end + + def load_attributes + result = DB.exec <<-SQL + SELECT + p.proname as name, + pg_catalog.pg_get_function_result(p.oid) as result_type, + pg_catalog.pg_get_function_arguments(p.oid) as args, + pg_catalog.pg_get_function_identity_arguments(p.oid) as arg_types, + CASE + WHEN p.proisagg THEN 'agg' + WHEN p.proiswindow THEN 'window' + WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger' + ELSE 'normal' + END as "type", + CASE + WHEN p.provolatile = 'i' THEN 'immutable' + WHEN p.provolatile = 's' THEN 'stable' + WHEN p.provolatile = 'v' THEN 'volatile' + END as volatility, + proisstrict as is_strict, + l.lanname as language, + p.prosrc as "source", + pg_catalog.obj_description(p.oid, 'pg_proc') as description, + CASE WHEN p.proisagg THEN 'agg_dummy' ELSE pg_get_functiondef(p.oid) END as definition + + FROM pg_catalog.pg_proc p + LEFT JOIN pg_catalog.pg_language l ON l.oid = p.prolang + WHERE pg_catalog.pg_function_is_visible(p.oid) + AND p.oid = #{oid}; + SQL + + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + + result.first + end + + def to_sql + definition.gsub("public.#{name}", name).strip + ';' + end + end + end +end diff --git a/lib/dumbo/pg_object/operator.rb b/lib/dumbo/pg_object/operator.rb new file mode 100644 index 0000000..08e6177 --- /dev/null +++ b/lib/dumbo/pg_object/operator.rb @@ -0,0 +1,74 @@ +module Dumbo + module PgObject + class Operator < Dumbo::PgObject::Base + attr_accessor :name, + :kind, + :hashes, + :merges, + :leftarg, + :rightarg, + :result_type, + :commutator, + :negator, + :function_name, + :join, + :restrict + + identfied_by :name, :leftarg, :rightarg + + def load_attributes + result = DB.exec <<-SQL + SELECT + op.oprname AS name, + op.oprkind AS kind, + op.oprcanhash AS hashes, + op.oprcanmerge AS merges, + lt.typname AS leftarg, + rt.typname AS rightarg, + et.typname AS result_type, + co.oprname AS commutator, + ne.oprname AS negator, + op.oprcode AS function_name, + op.oprjoin AS join, + op.oprrest AS restrict, + description + FROM pg_operator op + LEFT OUTER JOIN pg_type lt ON lt.oid=op.oprleft + LEFT OUTER JOIN pg_type rt ON rt.oid=op.oprright + JOIN pg_type et on et.oid=op.oprresult + LEFT OUTER JOIN pg_operator co ON co.oid=op.oprcom + LEFT OUTER JOIN pg_operator ne ON ne.oid=op.oprnegate + LEFT OUTER JOIN pg_description des ON des.objoid=op.oid + WHERE op.oid = #{oid} + SQL + + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + + result.first + end + + def to_sql + attrs = [:leftarg, :rightarg, :commutator, :negator, :restrict, :join].reduce([]) do |mem, attr| + mem << "#{attr.to_s.upcase} = #{public_send(attr)}" if public_send(attr) && public_send(attr) !='-' + mem + end + + attrs << "HASHES" if hashes == 't' + attrs << "MERGES" if merges == 't' + atttr_str = attrs.join(",\n ") + <<-SQL.gsub(/^ {6}/, '') + CREATE OPERATOR #{name} ( + PROCEDURE = #{function_name}, + #{atttr_str} + ); + SQL + end + + def drop + "DROP OPERATOR IF EXISTS #{name} (#{leftarg}, #{rightarg});" + end + end + end +end diff --git a/lib/dumbo/pg_object/type.rb b/lib/dumbo/pg_object/type.rb new file mode 100644 index 0000000..86942d9 --- /dev/null +++ b/lib/dumbo/pg_object/type.rb @@ -0,0 +1,35 @@ +module Dumbo + module PgObject + module Type + class Base < Dumbo::PgObject::Base + attr_accessor :name, :type, :typrelid + + identfied_by :name + + def load_attributes + result = DB.exec("SELECT typname, typtype, typrelid FROM pg_type WHERE oid = #{oid}").first + @name = result['typname'] + @type = result['typtype'] + @typrelid = result['typrelid'] + end + + def get + case type + when 'c' + PgObject::Type::CompositeType.new(oid) + when 'b' + PgObject::Type::BaseType.new(oid) + when 'r' + PgObject::Type::RangeType.new(oid) + when 'e' + PgObject::Type::EnumType.new(oid) + end + end + + def drop + "DROP TYPE IF EXISTS #{name};" + end + end + end + end +end diff --git a/lib/dumbo/pg_object/type/base_type.rb b/lib/dumbo/pg_object/type/base_type.rb new file mode 100644 index 0000000..63f778c --- /dev/null +++ b/lib/dumbo/pg_object/type/base_type.rb @@ -0,0 +1,75 @@ +module Dumbo + module PgObject + module Type + class BaseType < Dumbo::PgObject::Type::Base + attr_accessor :input_function, + :output_function, + :receive_function, + :send_function, + :analyze_function, + :category, + :default, + :alignment, + :storage, + :type, + :internallength, + :attribute_name, + :typrelid + + def load_attributes + sql = <<-SQL + SELECT + t.typname AS name, + t.typinput AS input_function, + t.typoutput AS output_function, + t.typreceive AS receive_function, + t.typsend AS send_function, + t.typanalyze AS analyze_function, + t.typcategory AS category, + t.typdefault AS default, + t.typrelid, + CASE WHEN t.typalign = 'i' THEN 'int' WHEN t.typalign = 'c' THEN 'char' WHEN t.typalign = 's' THEN 'short' WHEN t.typalign = 'd' THEN 'double' ELSE NULL END AS alignment, + CASE WHEN t.typstorage = 'p' THEN 'PLAIN' WHEN t.typstorage = 'e' THEN 'EXTENDED' WHEN t.typstorage = 'm' THEN 'MAIN' WHEN t.typstorage = 'x' THEN 'EXTENDED' ELSE NULL END AS storage, + t.typtype AS type, + t.typlen AS internallength, + format_type(t.oid, null) AS alias, e.typname as element, + description, ct.oid AS taboid, + (SELECT array_agg(label) FROM pg_seclabels sl1 WHERE sl1.objoid=t.oid) AS labels, + (SELECT array_agg(provider) FROM pg_seclabels sl2 WHERE sl2.objoid=t.oid) AS providers + FROM pg_type t + LEFT OUTER JOIN pg_type e ON e.oid=t.typelem + LEFT OUTER JOIN pg_class ct ON ct.oid = t.typrelid AND ct.relkind <> 'c' + LEFT OUTER JOIN pg_description des ON (des.objoid=t.oid AND des.classoid='pg_type'::regclass) + WHERE t.typtype != 'd' AND t.typnamespace = 2200::oid + AND ct.oid IS NULL + AND t.oid = #{oid} + SQL + + result = DB.exec(sql) + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + + result.first + end + + def to_sql + <<-SQL.gsub(/^ {8}/, '') + CREATE TYPE #{name}( + INPUT=#{input_function}, + OUTPUT=#{output_function}, + RECEIVE=#{receive_function}, + SEND=#{send_function}, + ANALYZE=#{analyze_function}, + CATEGORY='#{category}', + DEFAULT='#{default}', + INTERNALLENGTH=#{internallength}, + ALIGNMENT=#{alignment}, + STORAGE=#{storage} + ); + SQL + end + end + end + end +end diff --git a/lib/dumbo/pg_object/type/composite_type.rb b/lib/dumbo/pg_object/type/composite_type.rb new file mode 100644 index 0000000..3c44c08 --- /dev/null +++ b/lib/dumbo/pg_object/type/composite_type.rb @@ -0,0 +1,35 @@ +module Dumbo + module PgObject + module Type + class CompositeType < Dumbo::PgObject::Type::Base + attr_accessor :attributes + + def load_attributes + super + res = DB.exec <<-SQL + SELECT + attname, + format_type(t.oid,NULL) AS typname + FROM pg_attribute att + JOIN pg_type t ON t.oid=atttypid + + WHERE att.attrelid=#{typrelid} + ORDER by attnum + SQL + + attribute = Struct.new(:name, :type) + @attributes = res.map { |r| attribute.new(r['attname'], r['typname']) } + end + + def to_sql + attr_str = attributes.map { |a| "#{a.name} #{a.type}" }.join(",\n ") + <<-SQL.gsub(/^ {6}/, '') + CREATE TYPE #{name} AS ( + #{attr_str} + ); + SQL + end + end + end + end +end diff --git a/lib/dumbo/pg_object/type/enum_type.rb b/lib/dumbo/pg_object/type/enum_type.rb new file mode 100644 index 0000000..01c3dc8 --- /dev/null +++ b/lib/dumbo/pg_object/type/enum_type.rb @@ -0,0 +1,31 @@ +module Dumbo + module PgObject + module Type + class EnumType < Dumbo::PgObject::Type::Base + attr_accessor :labels + + def load_attributes + super + + res = DB.exec <<-SQL + SELECT enumlabel + FROM pg_enum + WHERE enumtypid = #{oid} + ORDER by enumsortorder + SQL + @labels = res.to_a.map { |r| r['enumlabel'] } + end + + def to_sql + lbl_str = labels.map { |l| "'" + l + "'" }.join(",\n ") + + <<-SQL.gsub(/^ {6}/, '') + CREATE TYPE #{name} AS ENUM ( + #{lbl_str} + ); + SQL + end + end + end + end +end diff --git a/lib/dumbo/pg_object/type/range_type.rb b/lib/dumbo/pg_object/type/range_type.rb new file mode 100644 index 0000000..17a2fe8 --- /dev/null +++ b/lib/dumbo/pg_object/type/range_type.rb @@ -0,0 +1,44 @@ +module Dumbo + module PgObject + module Type + class RangeType < Dumbo::PgObject::Type::Base + attr_accessor :subtype, :subtype_opclass, :collation, + :canonical, :subtype_diff + + def load_attributes + super + result = DB.exec <<-SQL + SELECT + st.typname AS subtype, + opc.opcname AS subtype_opclass, + col.collname AS collation, + rngcanonical AS canonical, + rngsubdiff AS subtype_diff + FROM pg_range + LEFT JOIN pg_type st ON st.oid=rngsubtype + LEFT JOIN pg_collation col ON col.oid=rngcollation + LEFT JOIN pg_opclass opc ON opc.oid=rngsubopc + WHERE rngtypid=#{oid} + SQL + + result.first.each do |k, v| + send("#{k}=", v) rescue nil + end + result.first + end + + def to_sql + attr_str = [:subtype, :subtype_opclass, :collation, :canonical, :subtype_diff].map do |a| + [a, public_send(a)] + end.select { |k, v| v && v != '-' }.map { |k, v| "#{k.upcase}=#{v}" }.join(",\n ") + + <<-SQL.gsub(/^ {6}/, '') + CREATE TYPE #{name} AS RANGE ( + #{attr_str} + ); + SQL + end + end + end + end +end diff --git a/lib/dumbo/rake_task.rb b/lib/dumbo/rake_task.rb deleted file mode 100644 index 1f9ee4f..0000000 --- a/lib/dumbo/rake_task.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'rake' -require 'rake/tasklib' -require 'erubis' -require 'pathname' -require 'yaml' -require 'logger' -require 'active_record' -require 'dumbo/extension' -require 'dumbo/dependency_resolver' -require 'rspec/core' -require 'dumbo/db_task' - -module Dumbo - class RakeTask < ::Rake::TaskLib - attr_accessor :name - - def initialize(name = 'dumbo') - @name = name - - namespace name do - Dumbo::DbTask.new(:db) - - desc 'creates and installs extension' - task all: [:src, Extension.file_name, :install] - - desc 'installs the extension' - task :install do - cmd = if ENV['DUMBO_USE_SUDO'] - 'make clean && make && sudo make install' - else - 'make clean && make && make install' - end - system(cmd) - fail "make failed with error check output" unless $?.success? - end - - desc 'concatenates files' - file Extension.file_name => file_list do |t| - sql = t.prerequisites.map do |file| - ["--source file #{file}"] + get_sql(file) + [' '] - end.flatten - concatenate sql, t.name - end - - desc 'prepare source files' - task :src do - Dir.glob('src/**/*.erb').each do |file| - src = convert_template(file) - out = Pathname.new(file).sub_ext('') - File.open(out.to_s, 'w') do |f| - f.puts src.join("\n") - end - end - end - - desc 'creates migration files for the last two versions' - task migrations: 'db:load_structure' do - old_version, new_version = Extension.versions.last(2).map(&:to_s) - - if new_version - ExtensionMigrator.new(Extension.name, old_version, new_version).create - end - end - - desc 'upgrate .control file to a new version' - task :new_version, :level do |t, args| - args.with_defaults(level: 'patch') - v = new_version(args[:level]) - Extension.version!(v) - - Rake::Task["#{name}:all"].invoke - end - end - end - - def new_version(level = :patch) - ExtensionVersion.new_from_string(Extension.version).bump(level).to_s - end - - # source sql file list - def file_list - DependencyResolver.new(Dir.glob('sql/**/*.{sql,erb}')).resolve - end - - def concatenate(lines, target) - File.open(target, 'w') do |f| - f.puts "-- complain if script is sourced in psql, rather than via CREATE EXTENSION" - f.puts "\\echo Use \"CREATE EXTENSION #{Extension.name}\" to load this file. \\quit" - lines.each do |line| - f.puts line unless line =~ DependencyResolver.depends_pattern - end - end - end - - def get_sql(file) - ext = Pathname.new(file).extname - if ext == '.erb' - convert_template(file) - else - File.readlines(file) - end - end - - def convert_template(file) - eruby = Erubis::Eruby.new(File.read(file)) - bindigs = get_bindings(file) - eruby.result(bindigs).split("\n") - end - - def get_bindings(file) - BindingLoader.new(file).load - end - end -end diff --git a/lib/dumbo/type.rb b/lib/dumbo/type.rb deleted file mode 100644 index e54d8c8..0000000 --- a/lib/dumbo/type.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Dumbo - class Type < PgObject - attr_accessor :name, :type, :typrelid - - identfied_by :name - - def load_attributes - result = DB.exec("SELECT typname, typtype, typrelid FROM pg_type WHERE oid = #{oid}").first - @name = result['typname'] - @type = result['typtype'] - @typrelid = result['typrelid'] - end - - def get - case type - when 'c' - Types::CompositeType.new(oid) - when 'b' - Types::BaseType.new(oid) - when 'r' - Types::RangeType.new(oid) - when 'e' - Types::EnumType.new(oid) - end - end - - def drop - "DROP TYPE IF EXISTS #{name};" - end - end -end diff --git a/lib/dumbo/types/base_type.rb b/lib/dumbo/types/base_type.rb deleted file mode 100644 index daf7b2b..0000000 --- a/lib/dumbo/types/base_type.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Dumbo - module Types - class BaseType < Dumbo::Type - attr_accessor :input_function, - :output_function, - :receive_function, - :send_function, - :analyze_function, - :category, - :default, - :alignment, - :storage, - :type, - :internallength, - :attribute_name, - :typrelid - - def load_attributes - sql = <<-SQL - SELECT - t.typname AS name, - t.typinput AS input_function, - t.typoutput AS output_function, - t.typreceive AS receive_function, - t.typsend AS send_function, - t.typanalyze AS analyze_function, - t.typcategory AS category, - t.typdefault AS default, - t.typrelid, - CASE WHEN t.typalign = 'i' THEN 'int' WHEN t.typalign = 'c' THEN 'char' WHEN t.typalign = 's' THEN 'short' WHEN t.typalign = 'd' THEN 'double' ELSE NULL END AS alignment, - CASE WHEN t.typstorage = 'p' THEN 'PLAIN' WHEN t.typstorage = 'e' THEN 'EXTENDED' WHEN t.typstorage = 'm' THEN 'MAIN' WHEN t.typstorage = 'x' THEN 'EXTENDED' ELSE NULL END AS storage, - t.typtype AS type, - t.typlen AS internallength, - format_type(t.oid, null) AS alias, e.typname as element, - description, ct.oid AS taboid, - (SELECT array_agg(label) FROM pg_seclabels sl1 WHERE sl1.objoid=t.oid) AS labels, - (SELECT array_agg(provider) FROM pg_seclabels sl2 WHERE sl2.objoid=t.oid) AS providers - FROM pg_type t - LEFT OUTER JOIN pg_type e ON e.oid=t.typelem - LEFT OUTER JOIN pg_class ct ON ct.oid = t.typrelid AND ct.relkind <> 'c' - LEFT OUTER JOIN pg_description des ON (des.objoid=t.oid AND des.classoid='pg_type'::regclass) - WHERE t.typtype != 'd' AND t.typnamespace = 2200::oid - AND ct.oid IS NULL - AND t.oid = #{oid} - SQL - - result = DB.exec(sql) - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - - result.first - end - - def to_sql - <<-SQL.gsub(/^ {8}/, '') - CREATE TYPE #{name}( - INPUT=#{input_function}, - OUTPUT=#{output_function}, - RECEIVE=#{receive_function}, - SEND=#{send_function}, - ANALYZE=#{analyze_function}, - CATEGORY='#{category}', - DEFAULT='#{default}', - INTERNALLENGTH=#{internallength}, - ALIGNMENT=#{alignment}, - STORAGE=#{storage} - ); - SQL - end - end - end -end diff --git a/lib/dumbo/types/composite_type.rb b/lib/dumbo/types/composite_type.rb deleted file mode 100644 index 7001df9..0000000 --- a/lib/dumbo/types/composite_type.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Dumbo - module Types - class CompositeType < Dumbo::Type - attr_accessor :attributes - - def load_attributes - super - res = DB.exec <<-SQL - SELECT - attname, - format_type(t.oid,NULL) AS typname - FROM pg_attribute att - JOIN pg_type t ON t.oid=atttypid - - WHERE att.attrelid=#{typrelid} - ORDER by attnum - SQL - - attribute = Struct.new(:name, :type) - @attributes = res.map { |r| attribute.new(r['attname'], r['typname']) } - end - - def to_sql - attr_str = attributes.map { |a| "#{a.name} #{a.type}" }.join(",\n ") - <<-SQL.gsub(/^ {6}/, '') - CREATE TYPE #{name} AS ( - #{attr_str} - ); - SQL - end - end - end -end diff --git a/lib/dumbo/types/enum_type.rb b/lib/dumbo/types/enum_type.rb deleted file mode 100644 index 83d8e6c..0000000 --- a/lib/dumbo/types/enum_type.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Dumbo - module Types - class EnumType < Dumbo::Type - attr_accessor :labels - - def load_attributes - super - - res = DB.exec <<-SQL - SELECT enumlabel - FROM pg_enum - WHERE enumtypid = #{oid} - ORDER by enumsortorder - SQL - @labels = res.to_a.map { |r| r['enumlabel'] } - end - - def to_sql - lbl_str = labels.map { |l| "'" + l + "'" }.join(",\n ") - - <<-SQL.gsub(/^ {6}/, '') - CREATE TYPE #{name} AS ENUM ( - #{lbl_str} - ); - SQL - end - end - end -end diff --git a/lib/dumbo/types/range_type.rb b/lib/dumbo/types/range_type.rb deleted file mode 100644 index 36408ae..0000000 --- a/lib/dumbo/types/range_type.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Dumbo - module Types - class RangeType < Dumbo::Type - attr_accessor :subtype, :subtype_opclass, :collation, - :canonical, :subtype_diff - - def load_attributes - super - result = DB.exec <<-SQL - SELECT - st.typname AS subtype, - opc.opcname AS subtype_opclass, - col.collname AS collation, - rngcanonical AS canonical, - rngsubdiff AS subtype_diff - FROM pg_range - LEFT JOIN pg_type st ON st.oid=rngsubtype - LEFT JOIN pg_collation col ON col.oid=rngcollation - LEFT JOIN pg_opclass opc ON opc.oid=rngsubopc - WHERE rngtypid=#{oid} - SQL - - result.first.each do |k, v| - send("#{k}=", v) rescue nil - end - result.first - end - - def to_sql - attr_str = [:subtype, :subtype_opclass, :collation, :canonical, :subtype_diff].map do |a| - [a, public_send(a)] - end.select { |k, v| v && v != '-' }.map { |k, v| "#{k.upcase}=#{v}" }.join(",\n ") - - <<-SQL.gsub(/^ {6}/, '') - CREATE TYPE #{name} AS RANGE ( - #{attr_str} - ); - SQL - end - end - end -end diff --git a/spec/aggregate_spec.rb b/spec/aggregate_spec.rb index c9460f8..becaa0c 100644 --- a/spec/aggregate_spec.rb +++ b/spec/aggregate_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Dumbo::Aggregate do +describe Dumbo::PgObject::Aggregate do let(:avg) do sql = <<-SQL SELECT p.oid @@ -9,7 +9,7 @@ WHERE proname='avg' AND pg_get_function_arguments(p.oid) = 'integer' SQL - Dumbo::Aggregate.new(Dumbo::DB.exec(sql).first['oid']) + described_class.new(Dumbo::DB.exec(sql).first['oid']) end let(:min) do @@ -20,7 +20,7 @@ WHERE proname='min' AND pg_get_function_arguments(p.oid) = 'integer' SQL - Dumbo::Aggregate.new(Dumbo::DB.exec(sql).first['oid']) + described_class.new(Dumbo::DB.exec(sql).first['oid']) end it 'avg should have a sql representation' do diff --git a/spec/cast_spec.rb b/spec/cast_spec.rb index c23b882..2acf297 100644 --- a/spec/cast_spec.rb +++ b/spec/cast_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Dumbo::Cast do +describe Dumbo::PgObject::Cast do let(:cast) do sql = <<-SQL SELECT ca.oid @@ -11,7 +11,7 @@ AND format_type(tt.oid,tt.typtypmod) = 'integer' SQL - Dumbo::Cast.new(Dumbo::DB.exec(sql).first['oid']) + described_class.new(Dumbo::DB.exec(sql).first['oid']) end it 'should have a sql representation' do diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb deleted file mode 100644 index 0036ea9..0000000 --- a/spec/cli_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe Dumbo::Cli do - describe '#bumb' do - specify do - Dumbo::Extension.stub(:name).and_return('myext') - Dumbo::Extension.stub(:version).and_return('0.1.1') - Dumbo::Extension.stub(:version!).and_return(nil) - - expect { Dumbo::Cli.new.bump('invalid-version') }.to raise_error(Dumbo::Cli::InvalidVersionLevel) - expect { Dumbo::Cli.new.bump('major') }.not_to raise_error - end - end -end diff --git a/spec/commands_spec.rb b/spec/commands_spec.rb new file mode 100644 index 0000000..b005e9e --- /dev/null +++ b/spec/commands_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +describe Dumbo::Command do + let(:root) { spec_root } + + describe Dumbo::Command::New do + around do |example| + FileUtils.rm_rf("#{root}/mytest") if File.exists?("#{root}/mytest") + dir = Dir.pwd + Dir.chdir("#{root}") + Dumbo::Command::New.exec('mytest', '0.2.0', 'Test') + example.run + Dir.chdir(dir) + FileUtils.rm_rf("#{root}/mytest") + end + + specify do + expect(File).to exist("#{root}/mytest/Makefile") + expect(File).to exist("#{root}/mytest/mytest.control") + expect(File).to exist("#{root}/mytest/README.md") + expect(File).to exist("#{root}/mytest/config/database.yml") + expect(File).to exist("#{root}/mytest/sql/mytest.sql") + expect(File).to exist("#{root}/mytest/src/mytest.c") + expect(File).to exist("#{root}/mytest/src/mytest.h") + expect(File).to exist("#{root}/mytest/test/sql/mytest_test.sql") + expect(File).to exist("#{root}/mytest/test/expected/mytest_test.out") + end + end + + describe Dumbo::Command::Bump do + around do |example| + FileUtils.rm_rf("#{root}/mytest") if File.exists?("#{root}/mytest") + dir = Dir.pwd + Dir.chdir("#{root}") + Dumbo::Command::New.exec('mytest', '0.2.0', 'Test') + Dir.chdir("#{root}/mytest") + example.run + Dir.chdir(dir) + FileUtils.rm_rf("#{root}/mytest") + end + + specify do + Dumbo::Command::Bump.exec('major') + expect(File.read('mytest.control')).to match /default_version = '1.0.0'/ + + Dumbo::Command::Bump.exec('minor') + expect(File.read('mytest.control')).to match /default_version = '1.1.0'/ + + Dumbo::Command::Bump.exec('patch') + expect(File.read('mytest.control')).to match /default_version = '1.1.1'/ + end + end + + describe Dumbo::Command::Build do + around do |example| + FileUtils.rm_rf("#{root}/mytest") if File.exists?("#{root}/mytest") + dir = Dir.pwd + Dir.chdir("#{root}") + Dumbo::Command::New.exec('mytest', '0.2.4', 'Test') + Dir.chdir("#{root}/mytest") + example.run + Dir.chdir(dir) + FileUtils.rm_rf("#{root}/mytest") + end + + specify do + Dumbo::Command::Build.exec + expect(File.read('mytest--0.2.4.sql')).to match /Use "CREATE EXTENSION mytest" to load this file/ + expect(File.read('mytest--0.2.4.sql')).to match /CREATE FUNCTION add_one/ + end + end + + describe Dumbo::Command::Migrations do + around do |example| + FileUtils.rm_rf("#{root}/mytest") if File.exists?("#{root}/mytest") + dir = Dir.pwd + Dir.chdir("#{root}") + Dumbo::Command::New.exec('mytest', '0.2.4', 'Test') + Dir.chdir("#{root}/mytest") + example.run + Dir.chdir(dir) + FileUtils.rm_rf("#{root}/mytest") + end + + let(:sql) do + <<-SQL + CREATE FUNCTION add_one(int) RETURNS int + AS '$libdir/mytest' + LANGUAGE C IMMUTABLE STRICT; + + CREATE FUNCTION fix_this(int) RETURNS int + AS $$ + BEGIN + RETURN 12; + END + $$ LANGUAGE PLPGSQL IMMUTABLE STRICT; + SQL + end + + specify do + Dumbo::Command::Build.exec + Dumbo::Command::Bump.exec('patch') + + File.open('sql/mytest.sql', 'w') { |file| file.puts(sql) } + + Dumbo::Command::Build.exec + Dumbo::Command::Migrations.exec + + expect(File.read('mytest--0.2.4--0.2.5.sql')).to match /CREATE OR REPLACE FUNCTION fix_this\(integer\)/ + expect(File.read('mytest--0.2.4--0.2.5.sql')).not_to match /add_one/ + expect(File.read('mytest--0.2.5--0.2.4.sql')).to match /DROP FUNCTION IF EXISTS fix_this\(integer\)/ + expect(File.read('mytest--0.2.4--0.2.5.sql')).not_to match /add_one/ + end + end +end diff --git a/spec/extension_spec.rb b/spec/extension_spec.rb index d5025fb..3c87a66 100644 --- a/spec/extension_spec.rb +++ b/spec/extension_spec.rb @@ -24,7 +24,13 @@ describe 'handling types' do let(:names) { %w(elephant_composite elephant_range elephant_enum) } - let(:classes) { [Dumbo::Types::EnumType, Dumbo::Types::CompositeType, Dumbo::Types::RangeType] } + let(:classes) do + [ + Dumbo::PgObject::Type::EnumType, + Dumbo::PgObject::Type::CompositeType, + Dumbo::PgObject::Type::RangeType + ] + end subject { extension.types } diff --git a/spec/operator_spec.rb b/spec/operator_spec.rb index 3f63488..1ff0b63 100644 --- a/spec/operator_spec.rb +++ b/spec/operator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Dumbo::Operator do +describe Dumbo::PgObject::Operator do let(:operator) do sql = <<-SQL SELECT oid FROM pg_operator @@ -9,7 +9,7 @@ AND format_type(oprright, NULL) = 'box' SQL - Dumbo::Operator.new(Dumbo::DB.exec(sql).first['oid']).get + described_class.new(Dumbo::DB.exec(sql).first['oid']).get end it 'should have a sql representation' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2189c8d..2887f91 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ require 'rubygems' +require 'pry' $LOAD_PATH.unshift File.expand_path('../..', __FILE__) require 'lib/dumbo' diff --git a/spec/support/extension_helper.rb b/spec/support/extension_helper.rb index 50baddb..734a0fc 100644 --- a/spec/support/extension_helper.rb +++ b/spec/support/extension_helper.rb @@ -5,8 +5,7 @@ def install_testing_extension mkdir -p #{spec_root}/dumbo_sample_runtime && \ cp -a #{spec_root}/dumbo_sample/* #{spec_root}/dumbo_sample_runtime cd #{spec_root}/dumbo_sample_runtime && \ - make -f #{spec_root}/dumbo_sample_runtime/Makefile && \ - make -f #{spec_root}/dumbo_sample_runtime/Makefile install + make && make install ) 1> /dev/null CMD end @@ -21,8 +20,6 @@ def uninstall_testing_extension CMD end - private - def spec_root File.join(File.dirname(__FILE__), '..') end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index e7e5bd6..20d0993 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Dumbo::Type do +describe Dumbo::PgObject::Type do around(:each) do |example| install_testing_extension extension.create @@ -14,7 +14,7 @@ let(:type) do oid = Dumbo::DB.exec("SELECT oid FROM pg_type WHERE typname = '#{type_name}'").first['oid'] - Dumbo::Type.new(oid).get + described_class::Base.new(oid).get end shared_examples_for 'identifiable' do diff --git a/template/Makefile.erb b/template/Makefile.erb index 4c3e225..88ecbef 100644 --- a/template/Makefile.erb +++ b/template/Makefile.erb @@ -14,11 +14,5 @@ OBJS = $(patsubst %.c,%.o,$(wildcard src/*.c)) TESTS = $(wildcard test/sql/*.sql) REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) -all: concat - -concat: - echo > $(EXTENSION)--$(EXTVERSION).sql - cat $(wildcard sql/*.sql) >> $(EXTENSION)--$(EXTVERSION).sql - REGRESS_OPTS = --inputdir=test --load-language=plpgsql include $(PGXS) diff --git a/template/sample.control.erb b/template/sample.control.erb index ab535ae..e4245b3 100644 --- a/template/sample.control.erb +++ b/template/sample.control.erb @@ -3,3 +3,4 @@ comment = '<%= extension_comment %>' default_version = '<%= initial_version %>' relocatable = true requires = '' +directory = 'extension'