Module: Prick::SubCommand

Extended by:
Timer
Defined in:
lib/prick.rb,
lib/subcommand/prick-fox.rb,
lib/subcommand/prick-drop.rb,
lib/subcommand/prick-init.rb,
lib/subcommand/prick-make.rb,
lib/subcommand/prick-build.rb,
lib/subcommand/prick-setup.rb,
lib/subcommand/prick-create.rb,
lib/subcommand/prick-migrate.rb,
lib/subcommand/prick-release.rb,
lib/subcommand/prick-teardown.rb

Class Method Summary collapse

Methods included from Timer

file, file=, new, off!, off?, on!, on?, scale, scale=, time, time, unit, unit=

Class Method Details

.build(database, username, schema, builddir: "schema", force: false, timer: nil, dump: nil) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/subcommand/prick-build.rb', line 6

def self.build(database, username, schema, builddir: "schema", force: false, timer: nil, dump: nil)
  Timer.on! if timer
  time "Prick::Command#build" do
    begin
      super_conn = PgConn.new # Used to create new databases (doesn't make a
                              # difference right now as the mikras user is
                              # still a superuser)

      conn = nil
      builder = nil
      time "Load build object" do
        if super_conn.rdbms.exist? database
          conn = PgConn.new(database, username)
          exist = true
        else
          super_conn.rdbms.create database, owner: username
          conn = PgConn.new(database, username)
          exist = false
        end

        builder = Prick::Build::Builder.new(conn, builddir)

        if exist
          if force
            # Drop all schemas but re-creates the public schema
            super_conn.rdbms.empty!(database)

          else
            # Find schemas to refresh. This includes all schemas in the
            # database
            refresh_schemas = conn.schema.list

            # Find existing keep-schemas (but exclude target schema)
            keep_schemas = builder.keep_schemas.select { |s| 
              conn.schema.exist?(s) && (schema.nil? || schema != s)
            }

            # Remove keep-schemas from list of schemas
            refresh_schemas -= keep_schemas

            # Eliminate keep schemas from build pool
            builder.pool.delete_schema(keep_schemas)

            # Drop refresh schemes
            refresh_schemas.each { |schema| conn.schema.drop(schema, cascade: true) }

            # Re-create public schema
            conn.schema.create("public", authorization: "postgres")
            conn.exec "grant usage, create on schema public to public"
          end
        end

        # Delete schemas after target scheme if present
        builder.pool.delete_schema(builder.pool.after_schema(schema)) if schema
      end
      
      case dump
        when :nodes; builder.nodes.reject { |node| node.is_a?(Build::BuildNode) }.map &:dump
        when :allnodes; builder.nodes.map &:dump
        when :batches; builder.dump
        when nil;
      else
        raise Prick::Error, "Illegal dump type: #{dump.inspect}"
      end && exit

      time "Execute build object" do
        builder.execute conn
      end

    rescue Prick::Error => ex
      $stderr.puts ex.message
      exit 1

    rescue ::Command::Error => ex
      $stderr.puts ex.message
      exit 1

    ensure
      super_conn&.terminate
      conn&.terminate
    end
  end
end

.create_migration(username, from_version, force: false, file: nil) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/subcommand/prick-create.rb', line 4

def self.create_migration(username, from_version, force: false, file: nil)
  constrain from_version, PrickVersion
  Git.clean? or raise "Won't migrate: Repository is dirty"
  Git.synchronized? or raise "Won't migrate: Repository is not synchronized"

  from_version != PrickVersion.zero or raise Prick::Error, "Can't migrate from release 0.0.0"
  to_version = Prick.state.version
  migration_dir = "#{MIGRATION_DIR}/#{from_version}"
  migration_exist = File.directory? migration_dir
  force || file || !migration_exist or raise Prick::Fail, "Migration #{from_version} exists"

  $quiet = true if file

  if file && migration_exist
    File.open(file, "w") { |file|
      for diff_file in DIFF_FILES
        file.write File.read("#{migration_dir}/#{diff_file}")
      end
    }
  else
    from_version != to_version or raise Prick::Fail, "Can't migrate to same release"
    from_version < to_version or raise Prick::Fail, "Can't migrate backwards (why not?)"

    diff = nil
    if force || !migration_exist
      from_db = "#{Prick.state.name}-#{from_version}"
      to_db = "#{Prick.state.name}-#{to_version}"

      mesg "Migrating from #{from_version} to #{to_version}"
      begin
        origin = Git.origin

        # Local repos are supported to ease testing
        if File.directory?(origin)
          origin = File.expand_path(origin)
        end

        diff = Dir.mktmpdir { |tmpdir|
            Dir.chdir(tmpdir) {
            mesg "  Building #{from_db}"
            Git.clone(origin, from_version, branch: from_version)
            Dir.chdir(from_version.to_s) { build(from_db, username, nil) }
          }

          mesg "  Building #{to_db}"
          build(to_db, username, nil)

          mesg "  Creating diff"
          Diff.new(from_db, to_db)
        }
        !diff.same? or raise Prick::Fail, "No changes"
      ensure
        drop_all(from_db)
        drop_all(to_db)
      end
    end

    diff.write(file) if file
  end

  if file
    File.open(file, "a") { |f| 
      f.puts "-- UPDATE VERSION"
      f.puts File.readlines(SCHEMA_VERSION_PATH).grep_v(/^--/)
    }
  else
    FileUtils.rm_rf migration_dir if force
    FileUtils.mkdir_p migration_dir
    Command.command "cp -a #{SHARE_PATH}/migrate/migration/. #{migration_dir}"
    Dir.chdir(migration_dir) { diff.write(*DIFF_FILES, mark: true) }
  end 
end

.drop_all(database) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/subcommand/prick-drop.rb', line 17

def self.drop_all(database)
  PgConn.new("postgres") { |conn|
    users = conn.role.list(database: database)
    PgConn.new(database) { |db| db.role.drop(users, cascade: true) }
    conn.rdbms.drop database
  }
end

.drop_database(database) ⇒ Object



13
14
15
# File 'lib/subcommand/prick-drop.rb', line 13

def self.drop_database(database)
  PgConn.new("postgres") { |conn| conn.rdbms.drop database }
end

.drop_users(database) ⇒ Object



6
7
8
9
10
11
# File 'lib/subcommand/prick-drop.rb', line 6

def self.drop_users(database)
  PgConn.new("postgres") { |conn|
    users = conn.role.list(database: database)
    conn.role.drop(users, cascade: true)
  }
end

.fox(database, username, files) ⇒ Object



6
7
8
# File 'lib/subcommand/prick-fox.rb', line 6

def self.fox(database, username, files)
  Command.command "fox --state=#{FOX_STATE_PATH} --exec #{database} #{files.join(" ")}"
end

.init(dir, name, title, database, username) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/subcommand/prick-init.rb', line 4

def self.init(dir, name, title, database, username)
  if dir
    !File.exist?(dir) or raise Prick::Error, "Directory #{dir} exists"
    FileUtils.mkdir_p(dir)
    Dir.chdir(dir)
  else
    dir = "."
  end
  name ||= File.basename(Dir.getwd)
  title ||= name
  database ||= name
  username ||= name

  Command.command %(
    git init .
    cp -a #{SHARE_PATH}/init/. .
    git add .
    git commit -am "Initial import"
  ), fail: false

  Command.status == 0 or raise Prick::Fail, "Init script failed"

  state = State.new
  state.name = name
  state.title = title
  state.prick_version = PrickVersion.new VERSION
  state.version = PrickVersion.new("0.0.0")
  state.environment = :development
  state.database = database
  state.username = username
  state.save

  Command.command %(
    set -e
    git commit -am "Release 0.0.0"
    git tag v0.0.0
  ), fail: false

  Command.status == 0 or raise Prick::Fail, "Init script failed"

  [dir, state]
end

.make(database, username, schema, timer: nil, dump: nil) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/subcommand/prick-make.rb', line 6

def self.make(database, username, schema, timer: nil, dump: nil)
  Timer.on! if timer
  time "Prick::Command#build" do
    begin
      super_conn = PgConn.new
      conn = nil
      builder = nil
      built_at = EPOCH
      clean = false
      create_schemas = []

      time "Load build object" do
        if super_conn.rdbms.exist? database
          conn = PgConn.new(database, username)
          if conn.schema.exist_table? "prick", "versions" && !conn.empty?("prick.versions")
            built_at = conn.value("select built_at from prick.versions")
          end
        else
          super_conn.rdbms.create database, owner: username
          conn = PgConn.new(database, username)
          clean = true
        end

        builder = Prick::Build::Builder.new(conn, "schema", clean)

        if schema
          builder.pool.after_schema(schema).each { |schema|
            conn.schema.drop(schema, cascade: true)
            builder.pool.delete_schema(schema)
          }
        end

        touched_nodes = builder.nodes.select { |node| File.mtime(node.path) > built_at }
        touched_phases = touched_nodes.map(&:phase).uniq.compact
        touched_kinds = touched_nodes.map(&:kind).uniq.compact
        touched_schema = touched_nodes.first&.schema
        missing_schema = builder.schemas.find { |schema| !conn.schema.exist?(schema) }
        build_schema = builder.schemas.find { |schema| [touched_schema, missing_schema].include? schema }

        if build_schema.nil?
          puts "#{database} is up to date"
          exit
        end

        if touched_phases.include?(:decl) || touched_phases.empty?
          create_schemas = [build_schema] + builder.pool.after_schema(build_schema)
          create_schemas.each { |schema| conn.schema.drop(schema, cascade: true) }
          builder.pool.delete_schema(builder.pool.before_schema(build_schema), exclude: [:seed])
        elsif touched_phases.include?(:init) || touched_phases.include?(:term)
          builder.pool.clear(:decl, :seed)
        elsif touched_phases.include?(:seed)
          builder.pool.clear(:init, :decl, :term)
        end

        builder.group
      end

      case dump
        when :nodes; builder.nodes.reject { |node| node.is_a?(Build::BuildNode) }.map &:dump
        when :allnodes; builder.nodes.map &:dump
        when :batches; builder.dump
        when nil;
      else
        raise Prick::Error, "Illegal dump type: #{dump.inspect}"
      end
      exit if dump

      time "Execute build object" do
        builder.execute(conn, create_schemas: create_schemas)
      end

    rescue Prick::Build::Error => ex
      $stderr.puts ex.message
      exit 1

    rescue Command::Error => ex
      $stderr.puts ex.message
      exit 1

    end
  end
end

.mesg(*msgs) ⇒ Object



27
# File 'lib/prick.rb', line 27

def self.mesg(*msgs) puts msgs.join(" ") if !$quiet end

.migrate(database, username, file: nil) ⇒ Object

def self.migrate(database, username, file) TODO



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/subcommand/prick-migrate.rb', line 5

def self.migrate(database, username, file: nil)
  PgConn.new(database, username) { |conn|
    conn.schema.exist_relation? "prick", "versions" or 
        raise Prick::Fail, "Can't read version from database"

    from_version = PrickVersion.new(conn.value "select version from prick.versions") or
        raise Prick::Fail, "Illegal version in table prick.versions"

    if file
      conn.transaction {
        conn.exec File.read(file)
      }
    else
      to_version = Prick.state.version
      from_version != to_version or raise Prick::Fail, "Already up to date"
      from_version < to_version or raise Prick::Fail, "Can't migrate backwards"

      migration_dir = "#{MIGRATION_DIR}/#{from_version}" 
      File.directory? migration_dir or 
          raise Prick::Fail, "Can't migrate from #{from_version} to #{to_version}"

      puts "Migrating from #{from_version} to #{to_version}"

      builder = Prick::Build::Builder.new(conn, migration_dir)
      conn.transaction {
        builder.execute conn
        conn.exec File.read(SCHEMA_VERSION_PATH)
      }
    end
  }
end

.release(kind) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/subcommand/prick-release.rb', line 4

def self.release(kind)
  constrain kind, :major, :minor, :patch

  Git.clean? or raise "Won't release: Repository is dirty"
  Git.synchronized? or raise "Won't release: Repository is not synchronized with origin"

  version = Prick.state.version.increment!(kind).to_s
  Prick.state.save

  Git.add(Prick.state.file)
  Git.add(Prick.state.schema_file)
  Git.commit "Release #{version}"
  Git.tag.create "v#{version}"
  Git.branch.create version
  Git.push

  puts version
end

.setup(database, username) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/subcommand/prick-setup.rb', line 6

def self.setup(database, username)
  conn = PgConn.new("template1") # Connect to template1 because database might not exist
  if !conn.role.exist? username
    # FIXME Should not be created as superuser but we can't do that before we
    # have a super: option in build
    conn.role.create username, superuser: true, can_login: true, create_role: true
  end
  if !conn.rdbms.exist? database
    conn.rdbms.create database, owner: username
  end
  builder = Prick::Build::Builder.new(conn, "schema")
  # TODO Run builder setup scrips
end

.teardown(database, username) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
# File 'lib/subcommand/prick-teardown.rb', line 6

def self.teardown(database, username)
  conn = PgConn.new "postgres" # Superuser connection
  if conn.rdbms.exist? database
    conn.rdbms.drop database
  end
  if conn.role.exist? username
    conn.role.drop username, cascade: true
  end
  builder = Prick::Build::Builder.new(conn, "schema")
  # TODO Run builder teardown scrips
end

.verb(*msgs) ⇒ Object



28
# File 'lib/prick.rb', line 28

def self.verb(*msgs) puts msgs.join(" ") if $verbose && !$quiet end