aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
blob: 0a637d81fa651359ceb2b264548c5f5cb5999a47 (plain) (blame)
1
2
3
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# frozen_string_literal: true

module ActiveRecord
  module ConnectionAdapters
    module SQLite3
      module SchemaStatements # :nodoc:
        # Returns an array of indexes for the given table.
        def indexes(table_name)
          exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
            # Indexes SQLite creates implicitly for internal use start with "sqlite_".
            # See https://www.sqlite.org/fileformat2.html#intschema
            next if row["name"].starts_with?("sqlite_")

            index_sql = query_value(<<~SQL, "SCHEMA")
              SELECT sql
              FROM sqlite_master
              WHERE name = #{quote(row['name'])} AND type = 'index'
              UNION ALL
              SELECT sql
              FROM sqlite_temp_master
              WHERE name = #{quote(row['name'])} AND type = 'index'
            SQL

            /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?\z/i =~ index_sql

            columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col|
              col["name"]
            end

            orders = {}

            if columns.any?(&:nil?) # index created with an expression
              columns = expressions
            else
              # Add info on sort order for columns (only desc order is explicitly specified,
              # asc is the default)
              if index_sql # index_sql can be null in case of primary key indexes
                index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
                  orders[order_column] = :desc
                }
              end
            end

            IndexDefinition.new(
              table_name,
              row["name"],
              row["unique"] != 0,
              columns,
              where: where,
              orders: orders
            )
          end.compact
        end

        def add_foreign_key(from_table, to_table, **options)
          alter_table(from_table) do |definition|
            to_table = strip_table_name_prefix_and_suffix(to_table)
            definition.foreign_key(to_table, options)
          end
        end

        def remove_foreign_key(from_table, to_table = nil, **options)
          to_table ||= options[:to_table]
          options = options.except(:name, :to_table)
          foreign_keys = foreign_keys(from_table)

          fkey = foreign_keys.detect do |fk|
            table = to_table || begin
              table = options[:column].to_s.delete_suffix("_id")
              Base.pluralize_table_names ? table.pluralize : table
            end
            table = strip_table_name_prefix_and_suffix(table)
            fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
            fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
          end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}")

          alter_table(from_table, foreign_keys) do |definition|
            fk_to_table = strip_table_name_prefix_and_suffix(fkey.to_table)
            definition.foreign_keys.delete([fk_to_table, fkey.options])
          end
        end

        def create_schema_dumper(options)
          SQLite3::SchemaDumper.create(self, options)
        end

        private
          def schema_creation
            SQLite3::SchemaCreation.new(self)
          end

          def create_table_definition(*args)
            SQLite3::TableDefinition.new(self, *args)
          end

          def new_column_from_field(table_name, field)
            default = \
              case field["dflt_value"]
              when /^null$/i
                nil
              when /^'(.*)'$/m
                $1.gsub("''", "'")
              when /^"(.*)"$/m
                $1.gsub('""', '"')
              else
                field["dflt_value"]
              end

            type_metadata = fetch_type_metadata(field["type"])
            Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"])
          end

          def data_source_sql(name = nil, type: nil)
            scope = quoted_scope(name, type: type)
            scope[:type] ||= "'table','view'"

            sql = +"SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'"
            sql << " AND name = #{scope[:name]}" if scope[:name]
            sql << " AND type IN (#{scope[:type]})"
            sql
          end

          def quoted_scope(name = nil, type: nil)
            type = \
              case type
              when "BASE TABLE"
                "'table'"
              when "VIEW"
                "'view'"
              end
            scope = {}
            scope[:name] = quote(name) if name
            scope[:type] = type if type
            scope
          end
      end
    end
  end
end