fias 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +142 -10
- data/fias.gemspec +3 -0
- data/lib/fias.rb +5 -2
- data/lib/fias/active_record/address_object.rb +170 -15
- data/lib/fias/active_record/address_object_type.rb +8 -1
- data/lib/fias/dbf_wrapper.rb +90 -0
- data/lib/fias/importer.rb +30 -0
- data/lib/fias/importer/base.rb +59 -0
- data/lib/fias/importer/pg.rb +81 -0
- data/lib/fias/importer/sqlite.rb +38 -0
- data/lib/fias/version.rb +1 -1
- data/lib/generators/fias/migration.rb +8 -4
- data/tasks/fias.rake +23 -26
- metadata +57 -6
- data/lib/fias/import/dbf_wrapper.rb +0 -83
- data/lib/fias/import/pg.rb +0 -144
data/README.md
CHANGED
@@ -16,33 +16,66 @@ gem 'fias', git: 'https://github.com/evilmartians/kladr.git'
|
|
16
16
|
|
17
17
|
2. Скачать и распаковать базу ФИАС в tmp/fias (по умолчанию)
|
18
18
|
|
19
|
-
#
|
19
|
+
# Импорт структуры данных
|
20
20
|
|
21
21
|
Возможны два варианта:
|
22
22
|
|
23
23
|
```
|
24
|
-
rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=...]
|
24
|
+
rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=... ONLY=...]
|
25
25
|
```
|
26
26
|
|
27
27
|
Либо:
|
28
28
|
|
29
29
|
```
|
30
|
-
rails g fias:migration [--path=... --prefix=...]
|
30
|
+
rails g fias:migration [--path=... --prefix=... --only=...]
|
31
31
|
```
|
32
32
|
|
33
33
|
Первый вариант - для использования гема вне рельсов, либо для случая, когда
|
34
34
|
актуальная база ФИАС будет использоваться только для локального мапинга, и
|
35
35
|
на продакшн попасть не должна.
|
36
36
|
|
37
|
+
В параметре only можно передать имена нужных таблиц, houses - все
|
38
|
+
таблицы домов.
|
39
|
+
|
37
40
|
# Импорт данных
|
38
41
|
|
39
42
|
```
|
40
|
-
rake fias:import
|
43
|
+
rake fias:import PREFIX=fias PATH=tmp/fias ONLY=address_objects
|
44
|
+
rake fias:import PREFIX=fias PATH=tmp/fias ONLY=houses
|
41
45
|
```
|
42
46
|
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
Первый пример импортирует только адресные объекты (без справочников),
|
48
|
+
второй - только дома.
|
49
|
+
|
50
|
+
Поддерживается импорт в Postgres и SQLite (нужно для :memory: баз)
|
51
|
+
|
52
|
+
# Импорт данных в память (для рейк тасок)
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
ActiveRecord::Base.configurations['fias'] = {
|
56
|
+
:adapter => 'sqlite3',
|
57
|
+
:database => ':memory:'
|
58
|
+
}
|
59
|
+
Fias::AddressObject.establish_connection :fias
|
60
|
+
|
61
|
+
fias = Fias::DbfWrapper.new('tmp/fias')
|
62
|
+
importer = Fias::Importer.build(
|
63
|
+
adapter: 'sqlite3', connection: Fias::AddressObject.connection.raw_connection
|
64
|
+
)
|
65
|
+
tables = fias.tables(:address_objects)
|
66
|
+
|
67
|
+
# Мигрейт. В таком виде так как стандартный мигратор всегда открывает новое
|
68
|
+
# соединение с БД, а в случае с SQLite это означает пересоздание базы :memory:.
|
69
|
+
Fias::AddressObject.connection.instance_exec do
|
70
|
+
eval(importer.schema(tables))
|
71
|
+
end
|
72
|
+
|
73
|
+
importer.import(tables) do |name, record, index|
|
74
|
+
record[:aclevel] == 1 # Только активные
|
75
|
+
end
|
76
|
+
|
77
|
+
Fias::AddressObject.count # И дальше импорт
|
78
|
+
```
|
46
79
|
|
47
80
|
# Некоторые замечания про ФИАС
|
48
81
|
|
@@ -55,24 +88,123 @@ rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
|
|
55
88
|
образования хранится в поле AOGUID. Из внешних таблиц логично ссылаться
|
56
89
|
на AOGUID, а не на AOID.
|
57
90
|
|
91
|
+
# Маппинг, он же итерирование
|
92
|
+
|
93
|
+
Задачи:
|
94
|
+
|
95
|
+
1. Сопоставить базу приложения с ФИАСом.
|
96
|
+
2. Уведомлять приложение об изменениях в связанных объектах ФИАСа.
|
97
|
+
3. Уведомлять приложения о новых данных в ФИАСе.
|
98
|
+
|
99
|
+
В таблице, с которой устанавливаются соответствия нужно создать поле для
|
100
|
+
хранения UUID записи ФИАСа.
|
101
|
+
|
102
|
+
На примере регионов:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# Будем искать соответствия регионов приложения регионам в ФИАС
|
106
|
+
scope = Region
|
107
|
+
fias_scope = Fias::AddressObject.leveled(:region)
|
108
|
+
|
109
|
+
# Обработчик событий итератора
|
110
|
+
matcher = ->(action, record, *objects) {
|
111
|
+
match = objects.first
|
112
|
+
|
113
|
+
case action
|
114
|
+
|
115
|
+
# Появилась новая запись в ФИАСе
|
116
|
+
when :created
|
117
|
+
puts "Region #{match.abbrevated}"
|
118
|
+
Region.create!(
|
119
|
+
fias_reference: match.id,
|
120
|
+
title: match.abbrevated
|
121
|
+
)
|
122
|
+
|
123
|
+
# Элемент в ФИАСе обновлен, нужно (?) обновить базу приложения
|
124
|
+
when :updated
|
125
|
+
record.update_attributes(
|
126
|
+
title: match.abbrevated,
|
127
|
+
fias_reference: match.id
|
128
|
+
)
|
129
|
+
puts "#{record.title} became #{match.abbrevated}"
|
130
|
+
|
131
|
+
# Объект в ФИАСе был, но из актуального состояния перешел в неактуальное.
|
132
|
+
# Типа, Припять: умерший город.
|
133
|
+
# Скорее всего, такой элемент в базе приложения нужно скрыть.
|
134
|
+
when :deleted
|
135
|
+
raise NotImplementedError, "#{record.title} #{objects.first.abbrevated}"
|
136
|
+
|
137
|
+
# Элемент в ФИАСе разделился: например, поселение Черто-Полохово
|
138
|
+
# было преобразовано в две деревни: Чертово и Полохово
|
139
|
+
# В этом случае параметр objects содержит объекты ФИАСа, на которые
|
140
|
+
# разделился старый.
|
141
|
+
when :split
|
142
|
+
raise NotImplementedError
|
143
|
+
|
144
|
+
# Элемент в ФИАСе объединился
|
145
|
+
# Типа, присоединили Московскую область к Москве
|
146
|
+
# В этом случае objects.first это запись ФИАС о новом объекте, а остальные
|
147
|
+
# элементы objects - записи ФИАС объединившихся объектов.
|
148
|
+
when :joined
|
149
|
+
raise NotImplementedError
|
150
|
+
|
151
|
+
# Элементу базы приложения нет соответствия в ФИАСе. Пытаемся сопоставить
|
152
|
+
# и зафиксировать, если найшли соответствующий вариант.
|
153
|
+
when :match
|
154
|
+
match = objects.detect { |b| b.actual? || b.name == record.title }
|
155
|
+
if match.present?
|
156
|
+
record.update_attributes(
|
157
|
+
title: match.abbrevated,
|
158
|
+
fias_reference: match.id
|
159
|
+
)
|
160
|
+
puts "#{record.title} became #{match.abbrevated} (new)"
|
161
|
+
else
|
162
|
+
puts "Record not found #{record.title}"
|
163
|
+
end
|
164
|
+
|
165
|
+
else
|
166
|
+
raise 'Unknown action!'
|
167
|
+
end
|
168
|
+
}
|
169
|
+
|
170
|
+
# Итерирует существующие в базе приложения элементы
|
171
|
+
# fias_reference - название колонки с UUID соответствующей региону записи
|
172
|
+
# ФИАСа
|
173
|
+
# title - название региона
|
174
|
+
fias_scope.match_existing(scope, :fias_reference, :title, &matcher)
|
175
|
+
|
176
|
+
# Итерирует отсутствующие в базе приложения, но имеющиеся в скоупе
|
177
|
+
# ФИАСа адреса.
|
178
|
+
fias_scope.actual.match_missing(scope, :fias_reference, &matcher)
|
179
|
+
```
|
180
|
+
|
181
|
+
Примечания:
|
182
|
+
|
183
|
+
1. В случае неоднозначного совпадения ФИАС и приложения, нужно ставить поиск
|
184
|
+
соответствия на модерацию.
|
185
|
+
2. В случае :split, :joined - тоже на модерацию. Хотя, разделения происходят
|
186
|
+
и нечасто, однако, в ФИАСе таких случаев больше полутора тысяч.
|
187
|
+
3. При начальном импорте данных из ФИАСа, #match_missing лучше начать вызывать
|
188
|
+
только после того как соответствия старой базы ФИАСу будут полностью разрулены.
|
189
|
+
|
58
190
|
# Работа с данными
|
59
191
|
|
60
192
|
Существующие регионы:
|
61
193
|
|
62
|
-
```
|
194
|
+
```ruby
|
63
195
|
Fias::AddressObject.actual.leveled(:region).all
|
64
196
|
```
|
65
197
|
|
66
198
|
Подчиненные объекты в регионе (области, районы, столица региона):
|
67
199
|
|
68
|
-
```
|
200
|
+
```ruby
|
69
201
|
region = Fias::AddressObject.actual.leveled(:region).first
|
70
202
|
region.children.actual
|
71
203
|
```
|
72
204
|
|
73
205
|
# TODO
|
74
206
|
|
75
|
-
1. Индексы.
|
207
|
+
1. Индексы. Индексы. ИНДЕКСЫ.
|
76
208
|
|
77
209
|
# Полезные ссылки
|
78
210
|
|
data/fias.gemspec
CHANGED
@@ -21,4 +21,7 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.add_dependency 'rake'
|
22
22
|
gem.add_dependency 'activerecord', '> 3'
|
23
23
|
gem.add_dependency 'progress_bar'
|
24
|
+
gem.add_development_dependency 'pg'
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
26
|
+
gem.add_development_dependency 'hirb'
|
24
27
|
end
|
data/lib/fias.rb
CHANGED
@@ -3,8 +3,11 @@ require 'dbf'
|
|
3
3
|
require 'active_record'
|
4
4
|
|
5
5
|
require 'fias/version'
|
6
|
-
require 'fias/
|
7
|
-
require 'fias/
|
6
|
+
require 'fias/dbf_wrapper'
|
7
|
+
require 'fias/importer'
|
8
|
+
require 'fias/importer/base'
|
9
|
+
require 'fias/importer/pg'
|
10
|
+
require 'fias/importer/sqlite'
|
8
11
|
|
9
12
|
require 'fias/railtie' if defined?(Rails)
|
10
13
|
|
@@ -1,9 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# TODO: Поделить на файлы
|
1
3
|
module Fias
|
2
4
|
class AddressObject < ActiveRecord::Base
|
3
|
-
|
5
|
+
# TODO: Тут надо понять как префикс передать
|
6
|
+
if defined?(Rails)
|
7
|
+
self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
|
8
|
+
else
|
9
|
+
self.table_name = "fias_address_objects"
|
10
|
+
end
|
11
|
+
|
4
12
|
self.primary_key = 'aoid'
|
5
13
|
|
6
14
|
alias_attribute :name, :formalname
|
15
|
+
alias_attribute :id, :aoid
|
7
16
|
|
8
17
|
# Родительские объекты (Ленобласть для Лодейнопольского района)
|
9
18
|
# Для "проезд 1-й Конной Лахты 2-й" - Санкт-Петербург и Ленинград.
|
@@ -20,17 +29,25 @@ module Fias
|
|
20
29
|
primary_key: 'aoguid',
|
21
30
|
foreign_key: 'parentguid'
|
22
31
|
|
23
|
-
#
|
24
|
-
|
32
|
+
# Предыдущие исторические версии названия (Ленинград для Питера)
|
33
|
+
# Может быть несколько, если произошло слияние
|
34
|
+
has_many :previous_versions,
|
25
35
|
class_name: 'AddressObject',
|
26
|
-
|
27
|
-
|
36
|
+
foreign_key: 'aoid',
|
37
|
+
primary_key: 'previd'
|
28
38
|
|
29
|
-
# Следующая
|
30
|
-
|
39
|
+
# Следующая исторические версии названия (Питер для Ленинграда)
|
40
|
+
# Может быть несколько, если произошло разделение
|
41
|
+
has_many :next_versions,
|
31
42
|
class_name: 'AddressObject',
|
32
|
-
|
33
|
-
|
43
|
+
foreign_key: 'aoid',
|
44
|
+
primary_key: 'nextid'
|
45
|
+
|
46
|
+
# Полное наименование типа объекта (город, улица)
|
47
|
+
belongs_to :address_object_type,
|
48
|
+
class_name: '::Fias::AddressObjectType',
|
49
|
+
foreign_key: 'shortname',
|
50
|
+
primary_key: 'scname'
|
34
51
|
|
35
52
|
# Актуальные записи (активные в настоящий момент)
|
36
53
|
# Проверено, что livestatus уже достаточен для идентификации
|
@@ -45,18 +62,33 @@ module Fias
|
|
45
62
|
}
|
46
63
|
|
47
64
|
scope :sorted, order('formalname ASC')
|
65
|
+
scope :with_types, includes(:address_object_type)
|
66
|
+
scope :matching, ->(name) {
|
67
|
+
scope = if self.connection.adapter_name
|
68
|
+
where(%{ "formalname" @@ ? OR ? @@ "formalname"}, name, name)
|
69
|
+
else
|
70
|
+
where('formalname LIKE ?', "%#{name}%")
|
71
|
+
end
|
48
72
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
73
|
+
# Первыми идут центральные и крупные поселения
|
74
|
+
scope.order('aolevel ASC, centstatus DESC')
|
75
|
+
}
|
76
|
+
|
77
|
+
scope :same_region, ->(address_object) {
|
78
|
+
where(regioncode: address_object.regioncode)
|
79
|
+
}
|
80
|
+
|
81
|
+
# Значимые поселения
|
82
|
+
scope :central, where('centstatus > 0')
|
54
83
|
|
55
|
-
# Есть ли исторические варианты записи?
|
56
84
|
def has_history?
|
57
85
|
previd.present?
|
58
86
|
end
|
59
87
|
|
88
|
+
def actual?
|
89
|
+
livestatus == 1
|
90
|
+
end
|
91
|
+
|
60
92
|
# Актуальный родитель. Для 1-го проезда 2-й Конной Лахты - только Питер.
|
61
93
|
def parent
|
62
94
|
parents.actual.first
|
@@ -66,11 +98,134 @@ module Fias
|
|
66
98
|
AOLEVELS.key(aolevel)
|
67
99
|
end
|
68
100
|
|
101
|
+
# Название с сокращением / полным наименованием города
|
102
|
+
# "г. Санкт-Петербург" / "город Санкт-Петербург"
|
103
|
+
#
|
104
|
+
# Для работы метода требуется загрузить таблицу :address_object_types
|
105
|
+
#
|
106
|
+
# Параметр - режим:
|
107
|
+
# :obviously - очевидный режим". В этом режиме "Дагестан" вместо
|
108
|
+
# "Республика Дагестан", "АО" вместо "Автономная область",
|
109
|
+
# но всегда "Краснодарский край"
|
110
|
+
# :short - дописываются "г." и "респ."
|
111
|
+
# :long - дописываются "город" и "Республика"
|
112
|
+
#
|
113
|
+
# Пример:
|
114
|
+
# .abbrevated(:obviously) # Тульская область, Хакасия, Еврейская Аобл.
|
115
|
+
# .abbrevated(:short) # Тульская область, Респ. Хакасия, Еврейская Аобл.
|
116
|
+
# .abbrevated(:long) # Тульская область, Республика Хакасия, Еврейская автономная область
|
117
|
+
def abbrevated(mode = :obviously)
|
118
|
+
return name if address_object_type.blank? || shortname.blank?
|
119
|
+
|
120
|
+
case aolevel_sym
|
121
|
+
when :region
|
122
|
+
# "Ханты-Мансийский Автономный округ - Югра"
|
123
|
+
return name if regioncode == '86'
|
124
|
+
|
125
|
+
ending = name[-2..-1]
|
126
|
+
|
127
|
+
# Если название кончается на -ая -ий - по правилам русского языка
|
128
|
+
# нужно дописать в конец "область", "край"
|
129
|
+
must_append = SHN_MUST_APPEND_TO_ENDINGS.include?(ending) ||
|
130
|
+
shortname == 'Чувашия'
|
131
|
+
|
132
|
+
must_abbrevate = must_append ||
|
133
|
+
shortname == 'Чувашия' ||
|
134
|
+
mode != :obviously
|
135
|
+
|
136
|
+
return name unless must_abbrevate
|
137
|
+
|
138
|
+
abbr = case mode
|
139
|
+
when :short
|
140
|
+
shortname
|
141
|
+
when :long
|
142
|
+
address_object_type.name
|
143
|
+
when :obviously
|
144
|
+
if SHN_LEAVE_SHORTS_INTACT.include?(shortname)
|
145
|
+
shortname
|
146
|
+
else
|
147
|
+
address_object_type.name
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Точка не ставится для АО, края, Чувашии и длинных названий
|
152
|
+
abbr = "#{abbr}." if mode == :short && not(shortname.in?(SHN_MUST_NOT_APPEND_DOT))
|
153
|
+
|
154
|
+
if not(must_append) && must_abbrevate
|
155
|
+
"#{abbr} #{name}"
|
156
|
+
else
|
157
|
+
# "Республика" => "республика", но "АО" остается
|
158
|
+
abbr = abbr.mb_chars.downcase if not(shortname.in?(SHN_LEAVE_SHORTS_INTACT))
|
159
|
+
"#{name} #{abbr}"
|
160
|
+
end
|
161
|
+
else
|
162
|
+
abbr = if mode == :long
|
163
|
+
address_object_type.try(:name)
|
164
|
+
else
|
165
|
+
shortname
|
166
|
+
end
|
167
|
+
"#{abbr} #{name}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class << self
|
172
|
+
# Подробное описание см. в README
|
173
|
+
# TODO: OPERSTATUS
|
174
|
+
def match_existing(scope, fias_key_accessor, title_field_accessor, &block)
|
175
|
+
scope.find_each do |record|
|
176
|
+
aoid = record.send(fias_key_accessor)
|
177
|
+
title = record.send(title_field_accessor)
|
178
|
+
|
179
|
+
if aoid.present?
|
180
|
+
match = scoped.find_by_aoid(aoid)
|
181
|
+
|
182
|
+
if match.present?
|
183
|
+
unless match.actual?
|
184
|
+
next_versions = match.next_versions
|
185
|
+
|
186
|
+
if next_versions.empty?
|
187
|
+
yield(:deleted, record, match)
|
188
|
+
elsif next_versions.count == 1
|
189
|
+
next_version = next_versions.first
|
190
|
+
previous_versions_of_current = next_version.previous_versions
|
191
|
+
|
192
|
+
if previous_versions_of_current.count == 1
|
193
|
+
yield(:updated, record, next_version)
|
194
|
+
else
|
195
|
+
yield(:joined, record, next_version, previous_versions_of_current)
|
196
|
+
end
|
197
|
+
elsif next_versions.count > 1
|
198
|
+
yield(:split, record, next_versions)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
else
|
203
|
+
matches = scoped.matching(title)
|
204
|
+
yield(:match, record, *matches)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def match_missing(scope, address_object_key_field, &block)
|
210
|
+
scoped.each do |address_object|
|
211
|
+
unless scope.where(address_object_key_field => address_object.aoid).exists?
|
212
|
+
yield(:created, nil, address_object)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
69
218
|
# Коды уровня адресного объекта
|
70
219
|
AOLEVELS = {
|
71
220
|
region: 1, autonomy: 2, district: 3, city: 4,
|
72
221
|
territory: 5, settlement: 6, street: 7,
|
73
222
|
additional_territory: 90, additional_territory_slave: 91
|
74
223
|
}
|
224
|
+
|
225
|
+
# Дописывать сокращения обязательно, "Самарская" выглядит странно,
|
226
|
+
# всегда должно быть "Самарская область", а "Дагестан" понятно и так.
|
227
|
+
SHN_MUST_APPEND_TO_ENDINGS = %w(ая ий)
|
228
|
+
SHN_MUST_NOT_APPEND_DOT = %w(край АО Чувашия) # Не дописывать точку к сокращению
|
229
|
+
SHN_LEAVE_SHORTS_INTACT = %w(АО Аобл Чувашия) # В очевидном режиме не разворачивать "АО" в "Автономная область"
|
75
230
|
end
|
76
231
|
end
|
@@ -1,8 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
module Fias
|
2
3
|
class AddressObjectType < ActiveRecord::Base
|
3
|
-
|
4
|
+
# TODO: Тут надо понять как префикс передать
|
5
|
+
if defined?(Rails)
|
6
|
+
self.table_name = "#{Rails.application.config.fias.prefix}_address_object_types"
|
7
|
+
else
|
8
|
+
self.table_name = "fias_address_object_types"
|
9
|
+
end
|
4
10
|
self.primary_key = 'scname'
|
5
11
|
|
6
12
|
alias_attribute :name, :socrname
|
13
|
+
alias_attribute :abbrevation, :scname
|
7
14
|
end
|
8
15
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Fias
|
2
|
+
# Класс для доступа к DBF-файлам ФИАС.
|
3
|
+
#
|
4
|
+
# Пример:
|
5
|
+
# wrapper = Fias::DbfWrapper.new('tmp/fias')
|
6
|
+
# wrapper.address_objects.record_count
|
7
|
+
# wrapper.address_objects.each { |record| record.attributes }
|
8
|
+
#
|
9
|
+
# TODO: Добавить в инишилайзер tables, чтобы при создании проверять
|
10
|
+
# их наличие на диске
|
11
|
+
class DbfWrapper
|
12
|
+
# Открывает DBF-файлы ФИАСа
|
13
|
+
def initialize(pathspec)
|
14
|
+
unless Dir.exists?(pathspec)
|
15
|
+
raise ArgumentError, 'FIAS database path does not exists'
|
16
|
+
end
|
17
|
+
self.pathspec = pathspec
|
18
|
+
|
19
|
+
DBF_ACCESSORS.each do |accessor, dbf_name|
|
20
|
+
filename = File.join(pathspec, dbf_name)
|
21
|
+
|
22
|
+
if File.exists?(filename)
|
23
|
+
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
24
|
+
send("#{accessor}=", dbf)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Возвращает хеш {аксессор dbf-таблицы => таблица}
|
30
|
+
# На входе массив названий таблиц (строки), без параметров - все
|
31
|
+
# Макрос houses вернет все таблицы домов
|
32
|
+
#
|
33
|
+
# Пример:
|
34
|
+
# wrapper = Fias::DbfWrapper.new('tmp/fias')
|
35
|
+
# wrapper.tables(%w(address_objects address_object_types houses))
|
36
|
+
# ... {
|
37
|
+
# ... address_objects: Dbf::Table(...),
|
38
|
+
# ... address_object_types: Dbf::Table(...),
|
39
|
+
# ... house1: Dbf::Table(...),
|
40
|
+
# ... ...
|
41
|
+
# ... house99: Dbf::Table(...)
|
42
|
+
# ... }
|
43
|
+
def tables(*only)
|
44
|
+
only = only.first if only.first.is_a?(Array)
|
45
|
+
only = only.map(&:to_s)
|
46
|
+
|
47
|
+
hash = DBF_ACCESSORS.keys.map do |accessor|
|
48
|
+
accessor_s = accessor.to_s
|
49
|
+
is_houses = only.include?('houses') && accessor_s.starts_with?('house')
|
50
|
+
|
51
|
+
if only.include?(accessor_s) || is_houses
|
52
|
+
[accessor, send(accessor)]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
Hash[*hash.compact.flatten]
|
56
|
+
end
|
57
|
+
|
58
|
+
# { house01: "HOUSE01"..house99: "HOUSE99" }
|
59
|
+
HOUSES_ACCESSORS = Hash[*(1..99).map { |n|
|
60
|
+
[("house%0.2d" % n).to_sym, "HOUSE%0.2d.dbf" % n]
|
61
|
+
}.flatten]
|
62
|
+
|
63
|
+
# Таблица соответствий аттрибутов класса DBF-файлам
|
64
|
+
DBF_ACCESSORS = {
|
65
|
+
address_object_types: 'SOCRBASE.DBF',
|
66
|
+
current_statuses: 'CURENTST.DBF',
|
67
|
+
actual_statuses: 'ACTSTAT.DBF',
|
68
|
+
operation_statuses: 'OPERSTAT.DBF',
|
69
|
+
center_statuses: 'CENTERST.DBF',
|
70
|
+
interval_statuses: 'INTVSTAT.DBF',
|
71
|
+
estate_statues: 'ESTSTAT.DBF',
|
72
|
+
structure_statuses: 'STRSTAT.DBF',
|
73
|
+
address_objects: 'ADDROBJ.DBF',
|
74
|
+
house_intervals: 'HOUSEINT.DBF',
|
75
|
+
landmarks: 'LANDMARK.DBF'
|
76
|
+
}.merge(
|
77
|
+
HOUSES_ACCESSORS
|
78
|
+
)
|
79
|
+
|
80
|
+
DEFAULT_ENCODING = Encoding::CP866
|
81
|
+
|
82
|
+
attr_reader *DBF_ACCESSORS.keys
|
83
|
+
attr_reader :houses
|
84
|
+
|
85
|
+
private
|
86
|
+
attr_accessor :pathspec
|
87
|
+
attr_writer *DBF_ACCESSORS.keys
|
88
|
+
attr_writer :houses
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Фактори-метод, возвращает ссылку на объект импортера
|
4
|
+
# Принимает параметры:
|
5
|
+
# :adapter - название адаптера
|
6
|
+
# :connection - прямое соединение с базой данных (connection.raw_connection)
|
7
|
+
def self.build(options = {})
|
8
|
+
adapter = options.delete(:adapter) ||
|
9
|
+
ActiveRecord::Base.connection_config[:adapter]
|
10
|
+
|
11
|
+
connection = options.delete(:connection) ||
|
12
|
+
ActiveRecord::Base.connection.raw_connection
|
13
|
+
|
14
|
+
case adapter
|
15
|
+
when 'postgresql'
|
16
|
+
Pg.new(
|
17
|
+
connection,
|
18
|
+
options
|
19
|
+
)
|
20
|
+
when 'sqlite3'
|
21
|
+
Sqlite.new(
|
22
|
+
connection,
|
23
|
+
options
|
24
|
+
)
|
25
|
+
else
|
26
|
+
raise 'Only postgres & sqlite supported now, fork'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
class Base
|
4
|
+
def initialize(connection, options = {})
|
5
|
+
self.prefix = options.delete(:prefix) || 'fias'
|
6
|
+
self.connection = connection
|
7
|
+
end
|
8
|
+
|
9
|
+
# На входе - хеш (имя таблицы => dbf)
|
10
|
+
def schema(tables)
|
11
|
+
"".tap do |s|
|
12
|
+
tables.each do |name, dbf|
|
13
|
+
if dbf.present?
|
14
|
+
s << schema_for(name, table_name(name), dbf)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema_for(name, table_name, dbf)
|
21
|
+
"".tap do |s|
|
22
|
+
s << %{create_table "#{table_name}", id: false do |t|\n}
|
23
|
+
s << schema_columns(name, dbf)
|
24
|
+
s << "end\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def import(tables, &block)
|
29
|
+
tables.each do |name, dbf|
|
30
|
+
if dbf
|
31
|
+
import_table(name, table_name(name), dbf, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def import_table(name, table_name, dbf, &block)
|
37
|
+
raise NotImplementedError, 'Implement this in concrete class'
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
attr_accessor :prefix, :connection
|
42
|
+
|
43
|
+
private
|
44
|
+
def table_name(name)
|
45
|
+
"#{prefix}_#{name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def schema_columns(accessor, table)
|
49
|
+
"".tap do |s|
|
50
|
+
table.columns.each do |column|
|
51
|
+
column_name = column.name.downcase
|
52
|
+
column_def = column.schema_definition
|
53
|
+
s << " t.column #{column_def}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Класс для импорта данных из ФИАС в PostgreSQL.
|
4
|
+
# Используется COPY FROM STDIN.
|
5
|
+
class Pg < Base
|
6
|
+
def schema_for(name, table_name, dbf)
|
7
|
+
super + alter_table_to_pg_uuids(name, table_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def alter_table_to_pg_uuids(name, table_name)
|
12
|
+
"".tap do |s|
|
13
|
+
columns = CONVERT_TO_UUID[name]
|
14
|
+
if columns.present?
|
15
|
+
columns.each do |column|
|
16
|
+
s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def import_table(name, table_name, dbf, &block)
|
23
|
+
fields = table_fields(dbf)
|
24
|
+
columns = table_columns(fields)
|
25
|
+
|
26
|
+
truncate_table(table_name)
|
27
|
+
copy_from_stdin(table_name, columns)
|
28
|
+
|
29
|
+
dbf.each_with_index do |record, index|
|
30
|
+
should_import = yield(name, record.attributes, index) if block_given?
|
31
|
+
|
32
|
+
unless should_import === false
|
33
|
+
data = record.to_a
|
34
|
+
put_data(data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
put_copy_end
|
39
|
+
end
|
40
|
+
|
41
|
+
def truncate_table(table_name)
|
42
|
+
connection.exec "TRUNCATE TABLE #{table_name};"
|
43
|
+
end
|
44
|
+
|
45
|
+
def table_fields(table)
|
46
|
+
table.columns.map(&:name).map(&:downcase)
|
47
|
+
end
|
48
|
+
|
49
|
+
def table_columns(fields)
|
50
|
+
fields.join(', ')
|
51
|
+
end
|
52
|
+
|
53
|
+
def copy_from_stdin(table_name, columns)
|
54
|
+
sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
|
55
|
+
connection.exec(sql)
|
56
|
+
end
|
57
|
+
|
58
|
+
def put_data(data)
|
59
|
+
data.map! { |item| item == "" ? '-nil-' : item }
|
60
|
+
line = data.join("\t") + "\n"
|
61
|
+
connection.put_copy_data(line)
|
62
|
+
end
|
63
|
+
|
64
|
+
def put_copy_end
|
65
|
+
connection.put_copy_end
|
66
|
+
|
67
|
+
while res = connection.get_result
|
68
|
+
result_status = res.res_status(res.result_status)
|
69
|
+
unless result_status == 'PGRES_COMMAND_OK'
|
70
|
+
raise "Import failure: #{result_status}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Эти поля нужно отконвертировать в тип UUID после создания
|
77
|
+
CONVERT_TO_UUID = {
|
78
|
+
address_objects: %w(aoguid aoid previd nextid parentguid)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Fias
|
2
|
+
module Importer
|
3
|
+
# Нужно для :memory: баз в первую очередь
|
4
|
+
class Sqlite < Base
|
5
|
+
def import_table(name, table_name, dbf, &block)
|
6
|
+
truncate_table(table_name)
|
7
|
+
|
8
|
+
qmarks = ['?'] * dbf.columns.keys.size
|
9
|
+
qmarks = qmarks.join(', ')
|
10
|
+
|
11
|
+
dbf.each_with_index do |record, index|
|
12
|
+
data = record.attributes
|
13
|
+
|
14
|
+
should_import = yield(name, data, index) if block_given?
|
15
|
+
|
16
|
+
unless should_import === false
|
17
|
+
columns = data.keys.join(', ')
|
18
|
+
|
19
|
+
values = data.values.map do |value|
|
20
|
+
if value.is_a?(Date)
|
21
|
+
value.to_s
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
connection.execute("INSERT INTO #{table_name} (#{columns}) VALUES (#{qmarks});", values)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def truncate_table(table_name)
|
34
|
+
connection.execute "DELETE FROM #{table_name} WHERE 1"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/fias/version.rb
CHANGED
@@ -4,14 +4,18 @@ module Fias
|
|
4
4
|
|
5
5
|
class_option :prefix, type: :string, default: :fias, desc: 'Table names prefix'
|
6
6
|
class_option :path, type: :string, default: 'tmp/fias', desc: 'Path to FIAS dbfs'
|
7
|
+
class_option :only, type: :string, default: '', desc: 'Only tables'
|
7
8
|
|
8
9
|
source_root File.expand_path("../templates", __FILE__)
|
9
10
|
|
10
11
|
def generate_migration
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
only = options.only.split(',').map(&:strip)
|
13
|
+
wrapper = Fias::DbfWrapper.new(options.path)
|
14
|
+
importer = Fias::Importer.build(prefix: options.prefix)
|
15
|
+
|
16
|
+
tables = wrapper.tables(only)
|
17
|
+
@schema = importer.schema(tables)
|
18
|
+
@schema.gsub!("\n", "\n ")
|
15
19
|
|
16
20
|
migration_template 'create_fias_tables.rb', 'db/migrate/create_fias_tables'
|
17
21
|
end
|
data/tasks/fias.rake
CHANGED
@@ -25,46 +25,43 @@ namespace :fias do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
fias_path = ENV['FIAS'] || 'tmp/fias'
|
28
|
-
wrapper = Fias::
|
29
|
-
importer =
|
28
|
+
wrapper = Fias::DbfWrapper.new(fias_path)
|
29
|
+
importer = Fias::Importer.build(prefix: ENV['PREFIX'])
|
30
30
|
|
31
31
|
yield(wrapper, importer)
|
32
32
|
end
|
33
|
+
|
34
|
+
def only
|
35
|
+
only = ENV['ONLY'].to_s.split(',').map(&:strip)
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
|
-
desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL)'
|
39
|
+
desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL, EXCLUDE or ONLY tables)'
|
36
40
|
task may_be_rails(:create_tables) do
|
37
41
|
within_connection do |wrapper, importer|
|
38
|
-
|
42
|
+
tables = wrapper.tables(only)
|
43
|
+
# TODO: Добавить во враппер tables, это убрать
|
44
|
+
raise "DBF file not found for: #{key}" if tables.keys.any? { |key| key.nil? }
|
45
|
+
ActiveRecord::Schema.define do
|
46
|
+
eval(importer.schema(tables))
|
47
|
+
end
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
total_record_count = wrapper.tables.sum { |accessor, dbf| dbf.record_count }
|
47
|
-
|
48
|
-
puts 'Importing FIAS data...'
|
51
|
+
desc 'Import FIAS data'
|
52
|
+
task may_be_rails(:import) do
|
53
|
+
within_connection do |wrapper, importer|
|
54
|
+
tables = wrapper.tables(only)
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
bar.increment!
|
53
|
-
end
|
56
|
+
total_record_count = tables.sum do |accessor, dbf|
|
57
|
+
dbf.present? ? dbf.record_count : 0
|
54
58
|
end
|
55
|
-
end
|
56
59
|
|
57
|
-
|
58
|
-
task may_be_rails(:houses) do
|
59
|
-
within_connection do |wrapper, importer|
|
60
|
-
total_record_count = wrapper.houses.sum { |region, dbf| dbf.record_count }
|
60
|
+
puts 'Importing FIAS data...'
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
bar
|
65
|
-
importer.import_houses do
|
66
|
-
bar.increment!
|
67
|
-
end
|
62
|
+
bar = ProgressBar.new(total_record_count)
|
63
|
+
importer.import(tables) do
|
64
|
+
bar.increment!
|
68
65
|
end
|
69
66
|
end
|
70
67
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fias
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dbf
|
@@ -75,6 +75,54 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: pg
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: sqlite3
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: hirb
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
78
126
|
description: Ruby wrapper to FIAS database
|
79
127
|
email:
|
80
128
|
- gzigzigzeo@evilmartians.com
|
@@ -91,8 +139,11 @@ files:
|
|
91
139
|
- lib/fias.rb
|
92
140
|
- lib/fias/active_record/address_object.rb
|
93
141
|
- lib/fias/active_record/address_object_type.rb
|
94
|
-
- lib/fias/
|
95
|
-
- lib/fias/
|
142
|
+
- lib/fias/dbf_wrapper.rb
|
143
|
+
- lib/fias/importer.rb
|
144
|
+
- lib/fias/importer/base.rb
|
145
|
+
- lib/fias/importer/pg.rb
|
146
|
+
- lib/fias/importer/sqlite.rb
|
96
147
|
- lib/fias/railtie.rb
|
97
148
|
- lib/fias/version.rb
|
98
149
|
- lib/generators/fias/migration.rb
|
@@ -112,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
163
|
version: '0'
|
113
164
|
segments:
|
114
165
|
- 0
|
115
|
-
hash:
|
166
|
+
hash: 493497803985021140
|
116
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
168
|
none: false
|
118
169
|
requirements:
|
@@ -121,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
172
|
version: '0'
|
122
173
|
segments:
|
123
174
|
- 0
|
124
|
-
hash:
|
175
|
+
hash: 493497803985021140
|
125
176
|
requirements: []
|
126
177
|
rubyforge_project:
|
127
178
|
rubygems_version: 1.8.24
|
@@ -1,83 +0,0 @@
|
|
1
|
-
module Fias
|
2
|
-
module Import
|
3
|
-
# Класс для доступа к DBF-файлам ФИАС
|
4
|
-
# Пример:
|
5
|
-
# wrapper = DbfWrapper.new('tmp/fias')
|
6
|
-
# wrapper.address_objects.record_count
|
7
|
-
# wrapper.address_objects.each { |record| record.attributes }
|
8
|
-
class DbfWrapper
|
9
|
-
def initialize(pathspec)
|
10
|
-
unless Dir.exists?(pathspec)
|
11
|
-
raise ArgumentError, 'FIAS database path does not exists'
|
12
|
-
end
|
13
|
-
self.pathspec = pathspec
|
14
|
-
|
15
|
-
TABLES_ACCESSORS.each do |key, dbf_name|
|
16
|
-
filename = File.join(pathspec, dbf_name)
|
17
|
-
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
18
|
-
|
19
|
-
send("#{key}=", dbf)
|
20
|
-
end
|
21
|
-
|
22
|
-
self.houses = {}
|
23
|
-
Dir[File.join(pathspec, HOUSE_DBF_MASK)].each do |filename|
|
24
|
-
File.basename(filename) =~ /(\d+)/
|
25
|
-
region_code = $1.to_i
|
26
|
-
|
27
|
-
dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
|
28
|
-
self.houses[region_code] = dbf
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def tables
|
33
|
-
hash = TABLES_ACCESSORS.keys.map do |accessor|
|
34
|
-
[accessor, send(accessor)]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def build_importer(options)
|
39
|
-
config = ActiveRecord::Base.connection_config
|
40
|
-
if config[:adapter] == 'postgresql'
|
41
|
-
import = Fias::Import::PgImporter.new(
|
42
|
-
ActiveRecord::Base.connection.raw_connection,
|
43
|
-
self,
|
44
|
-
options
|
45
|
-
)
|
46
|
-
else
|
47
|
-
raise 'Only postgres supported now, fork'
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Таблица соответствий аттрибутов класса DBF-файлам
|
52
|
-
TABLES_ACCESSORS = {
|
53
|
-
address_object_types: 'SOCRBASE.DBF',
|
54
|
-
current_statuses: 'CURENTST.DBF',
|
55
|
-
actual_statuses: 'ACTSTAT.DBF',
|
56
|
-
operation_statuses: 'OPERSTAT.DBF',
|
57
|
-
center_statuses: 'CENTERST.DBF',
|
58
|
-
interval_statuses: 'INTVSTAT.DBF',
|
59
|
-
estate_statues: 'ESTSTAT.DBF',
|
60
|
-
structure_statuses: 'STRSTAT.DBF',
|
61
|
-
address_objects: 'ADDROBJ.DBF',
|
62
|
-
house_intervals: 'HOUSEINT.DBF',
|
63
|
-
landmarks: 'LANDMARK.DBF'
|
64
|
-
}
|
65
|
-
HOUSE_DBF_MASK = 'HOUSE??.DBF'
|
66
|
-
|
67
|
-
# Эти поля нужно отконвертировать в тип UUID после создания
|
68
|
-
CONVERT_TO_UUID = {
|
69
|
-
address_objects: %w(aoguid aoid previd nextid parentguid)
|
70
|
-
}
|
71
|
-
|
72
|
-
DEFAULT_ENCODING = Encoding::CP866
|
73
|
-
|
74
|
-
attr_reader *TABLES_ACCESSORS.keys
|
75
|
-
attr_reader :houses
|
76
|
-
|
77
|
-
private
|
78
|
-
attr_accessor :pathspec
|
79
|
-
attr_writer *TABLES_ACCESSORS.keys
|
80
|
-
attr_writer :houses
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
data/lib/fias/import/pg.rb
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
module Fias
|
2
|
-
module Import
|
3
|
-
class PgImporter
|
4
|
-
# Класс для импорта данных из ФИАС в PostgreSQL.
|
5
|
-
# Используется COPY FROM STDIN.
|
6
|
-
def initialize(raw_connection, wrapper, options = {})
|
7
|
-
self.prefix = options.delete(:prefix) || 'fias'
|
8
|
-
self.raw_connection = raw_connection
|
9
|
-
self.wrapper = wrapper
|
10
|
-
end
|
11
|
-
|
12
|
-
# Генерирует схему базы для ActiveRecord в виде строки
|
13
|
-
def schema
|
14
|
-
"".tap do |s|
|
15
|
-
wrapper.tables.each do |accessor, table|
|
16
|
-
s << %{create_table "#{table_name(accessor)}", id: false do |t|\n}
|
17
|
-
s << dump_schema_for(accessor, table)
|
18
|
-
s << "end\n"
|
19
|
-
|
20
|
-
s << alter_table_to_pg_uuids(accessor)
|
21
|
-
end
|
22
|
-
|
23
|
-
s << %{create_table "#{table_name('houses')}", id: false do |t|\n}
|
24
|
-
s << dump_schema_for(:houses, wrapper.houses.values.first)
|
25
|
-
s << " t.column :regioncode, :integer\n"
|
26
|
-
s << "end\n"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def import(&block)
|
31
|
-
wrapper.tables.each do |accessor, table|
|
32
|
-
import_table(accessor, table, &block)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def import_houses(&block)
|
37
|
-
wrapper.houses.each do |region, table|
|
38
|
-
import_houses_for_region(region, table, &block)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
def dump_schema_for(accessor, table)
|
44
|
-
"".tap do |s|
|
45
|
-
table.columns.each do |column|
|
46
|
-
column_name = column.name.downcase
|
47
|
-
column_def = column.schema_definition
|
48
|
-
s << " t.column #{column_def}"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def alter_table_to_pg_uuids(accessor)
|
54
|
-
"".tap do |s|
|
55
|
-
columns = wrapper.class::CONVERT_TO_UUID[accessor]
|
56
|
-
if columns.present?
|
57
|
-
columns.each do |column|
|
58
|
-
s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name(accessor)} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def import_table(accessor, table, &block)
|
65
|
-
fields = table_fields(table)
|
66
|
-
columns = table_columns(fields)
|
67
|
-
|
68
|
-
tn = table_name(accessor)
|
69
|
-
|
70
|
-
truncate_table(tn)
|
71
|
-
copy_from_stdin(tn, columns)
|
72
|
-
|
73
|
-
table.each_with_index do |record|
|
74
|
-
data = record.to_a
|
75
|
-
put_data(data)
|
76
|
-
yield(accessor, data) if block_given?
|
77
|
-
end
|
78
|
-
|
79
|
-
put_copy_end
|
80
|
-
end
|
81
|
-
|
82
|
-
def import_houses_for_region(region, table, &block)
|
83
|
-
fields = table_fields(table)
|
84
|
-
fields << :regioncode
|
85
|
-
columns = table_columns(fields)
|
86
|
-
|
87
|
-
tn = table_name('houses')
|
88
|
-
|
89
|
-
truncate_table(tn)
|
90
|
-
copy_from_stdin(tn, columns)
|
91
|
-
|
92
|
-
table.each_with_index do |record|
|
93
|
-
data = record.to_a
|
94
|
-
data << region
|
95
|
-
put_data(data)
|
96
|
-
yield('houses', data) if block_given?
|
97
|
-
end
|
98
|
-
|
99
|
-
put_copy_end
|
100
|
-
end
|
101
|
-
|
102
|
-
def table_name(table)
|
103
|
-
"#{prefix}_#{table}"
|
104
|
-
end
|
105
|
-
|
106
|
-
def table_fields(table)
|
107
|
-
table.columns.map(&:name).map(&:downcase)
|
108
|
-
end
|
109
|
-
|
110
|
-
def table_columns(fields)
|
111
|
-
fields.join(',')
|
112
|
-
end
|
113
|
-
|
114
|
-
def truncate_table(table_name)
|
115
|
-
raw_connection.exec "TRUNCATE TABLE #{table_name};"
|
116
|
-
end
|
117
|
-
|
118
|
-
def copy_from_stdin(table_name, columns)
|
119
|
-
sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
|
120
|
-
raw_connection.exec(sql)
|
121
|
-
end
|
122
|
-
|
123
|
-
def put_data(data)
|
124
|
-
data.map! { |item| item == "" ? '-nil-' : item }
|
125
|
-
line = data.join("\t") + "\n"
|
126
|
-
raw_connection.put_copy_data(line)
|
127
|
-
end
|
128
|
-
|
129
|
-
def put_copy_end
|
130
|
-
raw_connection.put_copy_end
|
131
|
-
|
132
|
-
while res = raw_connection.get_result
|
133
|
-
result_status = res.res_status(res.result_status)
|
134
|
-
unless result_status == 'PGRES_COMMAND_OK'
|
135
|
-
raise "Import failure: #{result_status}"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
attr_accessor :raw_connection, :wrapper
|
141
|
-
attr_accessor :prefix, :table_mappings
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|