exwiw 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/exe/exwiw +6 -0
- data/lib/exwiw/adapter/mysql2_adapter.rb +171 -0
- data/lib/exwiw/adapter/postgresql_adapter.rb +171 -0
- data/lib/exwiw/adapter/sqlite3_adapter.rb +165 -0
- data/lib/exwiw/adapter.rb +44 -0
- data/lib/exwiw/belongs_to.rb +14 -0
- data/lib/exwiw/cli.rb +150 -0
- data/lib/exwiw/determine_table_processing_order.rb +42 -0
- data/lib/exwiw/query_ast.rb +84 -0
- data/lib/exwiw/query_ast_builder.rb +125 -0
- data/lib/exwiw/railtie.rb +9 -0
- data/lib/exwiw/runner.rb +87 -0
- data/lib/exwiw/table_column.rb +19 -0
- data/lib/exwiw/table_config.rb +93 -0
- data/lib/exwiw/version.rb +5 -0
- data/lib/exwiw.rb +30 -0
- data/lib/tasks/exwiw.rake +54 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0aa1977437fc4e44349ecc11431e4f8697acb1471e159777c629850a52de1664
|
4
|
+
data.tar.gz: cd92c7a05f6958d2f0cc5ee1204b30fe7b6bd6e168aa18c0b2b182da3b00af9a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e979a144ac442f73483c23c93cb335742b43e9d198811788edb9a416a3d70cd3b23b64ab0783cf20b4452afa00d46827aaed629d021d880172d373491cc9ec96
|
7
|
+
data.tar.gz: 35375bb916081981ff264dd74a53b59638c78ad0e2f4dfb40d8716bec59caa387dc114001234be8bb2ba20a3345ecac9d88a1b7b0322ae38b8d92759c61efa75
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Shia
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# Exwiw
|
2
|
+
|
3
|
+
Export What I Want (Exwiw) is a Ruby gem that allows you to export records from a database to a dump file(to specifically, the full list of INSERT sql) on the specified conditions.
|
4
|
+
|
5
|
+
## When to use
|
6
|
+
|
7
|
+
Most of case in developing a software, There is no better choice than the same data in production.
|
8
|
+
You might make well-crafted data, but it's very very hard to maintain.
|
9
|
+
|
10
|
+
If you find the way to maintain the data for develoment env, then exwiw might be a solution for that.
|
11
|
+
|
12
|
+
- Export the full database and mask data and import to another database.
|
13
|
+
- Setup some system to replicate and mask data in real-time to another database.
|
14
|
+
|
15
|
+
|
16
|
+
You want to export only the data you want to export.
|
17
|
+
|
18
|
+
## Features
|
19
|
+
|
20
|
+
- Export the full list of INSERT sql for the specified conditions.
|
21
|
+
- Provide serveral masking options for sensitive columns.
|
22
|
+
- Provide config generator for ActiveRecord.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle add exwiw
|
28
|
+
```
|
29
|
+
|
30
|
+
Most of cases, you want to add 'require: false' to the Gemfile.
|
31
|
+
|
32
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
gem install exwiw
|
36
|
+
```
|
37
|
+
|
38
|
+
## Supported Databases
|
39
|
+
|
40
|
+
- mysql2
|
41
|
+
- postgresql
|
42
|
+
- sqlite3
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
### Command
|
47
|
+
|
48
|
+
```bash
|
49
|
+
# dump & masking all records from database to dump.sql based on schema.json
|
50
|
+
# pass database password as an environment variable 'DATABASE_PASSWORD'
|
51
|
+
exwiw \
|
52
|
+
--adapter=mysql2 \
|
53
|
+
--host=localhost \
|
54
|
+
--port=3306 \
|
55
|
+
--user=reader \
|
56
|
+
--database=app_production \
|
57
|
+
--config-dir=exwiw \
|
58
|
+
--target-table=shops \
|
59
|
+
--ids=1 \ # comma separated ids
|
60
|
+
--output-dir=dump \
|
61
|
+
--log-level=info
|
62
|
+
```
|
63
|
+
|
64
|
+
This command will generate sql files in the `dump` directory.
|
65
|
+
|
66
|
+
- `dump/insert-{idx}-{table_name}.sql`
|
67
|
+
- `dump/delete-{idx}-{table_name}.sql`
|
68
|
+
|
69
|
+
idx means the order of the dump. bigger idx might depend on smaller idx,
|
70
|
+
so you should import the dump in order.
|
71
|
+
|
72
|
+
you need to delete the records before importing the dump,
|
73
|
+
`delete-{idx}-{table_name}.sql` will help you to do that.
|
74
|
+
This sql will delete "all" related records to the extract targets.
|
75
|
+
idx meaning is the same as insert sql.
|
76
|
+
|
77
|
+
### Generator
|
78
|
+
|
79
|
+
the config generator is provided as Rake task.
|
80
|
+
|
81
|
+
```bash
|
82
|
+
# generate table schema under exwiw/
|
83
|
+
bundle exec rake exwiw:schema:generate
|
84
|
+
```
|
85
|
+
|
86
|
+
### Configuration
|
87
|
+
|
88
|
+
This is an example of the one table schema:
|
89
|
+
|
90
|
+
```json
|
91
|
+
{
|
92
|
+
"name": "users",
|
93
|
+
"primary_key": "id",
|
94
|
+
"filter": "users.id > 0",
|
95
|
+
"belongs_to": [{
|
96
|
+
"name": "companies",
|
97
|
+
"foreign_key": "company_id"
|
98
|
+
}],
|
99
|
+
"columns": [{
|
100
|
+
"name": "id",
|
101
|
+
}, {
|
102
|
+
"name": "email",
|
103
|
+
"replace_with": "user{id}@example.com"
|
104
|
+
}, {
|
105
|
+
"name": "company_id"
|
106
|
+
}]
|
107
|
+
}
|
108
|
+
```
|
109
|
+
|
110
|
+
`--config-dir` will use all json files in the specified directory.
|
111
|
+
|
112
|
+
### Filter
|
113
|
+
|
114
|
+
Some case, you don't need full records related to target. e.g. dump user access logs only for the last year.
|
115
|
+
`filter` is here for that. Be careful to use this option, as it will be:
|
116
|
+
|
117
|
+
- injected as it is in table condition(e.g. WHERE on mysql), so you are recommended to clearify table name of column to avoid ambiguity.
|
118
|
+
- injected to every where / join clause, so it affects to all tables depends on filterted target-table. it results to data inconsistency.
|
119
|
+
|
120
|
+
### Masking
|
121
|
+
|
122
|
+
`exwiw` provides several options for masking value.
|
123
|
+
|
124
|
+
#### `replace_with`
|
125
|
+
|
126
|
+
It will replace the value with the specified string,
|
127
|
+
and you can use the column name with `{}` to replace the value with the column value.
|
128
|
+
|
129
|
+
For example, Let assume we have the record which id is 1,
|
130
|
+
then "user{id}@example.com" will be replaced with "user1@example.com".
|
131
|
+
|
132
|
+
#### `raw_sql`
|
133
|
+
|
134
|
+
It will used instead of the original value.
|
135
|
+
|
136
|
+
For example, `"raw_sql": "CONCAT('user', shops.id, '@example.com')"` is equivalent to
|
137
|
+
`"replace_with": "user{id}@example.com"`.
|
138
|
+
This is useful when you want to transform with functions provided by the database.
|
139
|
+
|
140
|
+
Notice that you are recommended to clearify table name of column to avoid ambiguity.
|
141
|
+
|
142
|
+
If it used with `replace_with`, `replace_with` will be ignored.
|
143
|
+
|
144
|
+
#### `map`
|
145
|
+
|
146
|
+
XXX: TODO
|
147
|
+
|
148
|
+
Given value will be evaluated as Ruby code, and treated as the proc.
|
149
|
+
|
150
|
+
```
|
151
|
+
"map": "proc { |r| 'user' + v['id'].to_s + '@example.com' }"
|
152
|
+
```
|
153
|
+
|
154
|
+
which is equivalent to `"replace_with": "user{id}@example.com"`.
|
155
|
+
|
156
|
+
Notice this is the most powerful option, but you should be careful to use this option.
|
157
|
+
Because this transformation occured on exwiw process, so much slower than other options.
|
158
|
+
Most of case, this option is not recommended.
|
159
|
+
|
160
|
+
## How it works
|
161
|
+
|
162
|
+
- Load the table information from the specified config file.
|
163
|
+
- Calculate the dependency between tables.
|
164
|
+
- Generate the full list of INSERT sql based on the specified conditions.
|
165
|
+
- If the processing table has no relation with target tables, then dump all records.
|
166
|
+
- If the processing table has relation with target tables, then dump the records which are related to the target tables.
|
167
|
+
- Generate the full list of DELETE sql based on the specified conditions.
|
168
|
+
- If the processing table has no relation with target tables, then delete all records.
|
169
|
+
- If the processing table has relation with target tables, then delete the records which are related to the target tables.
|
170
|
+
|
171
|
+
## Development
|
172
|
+
|
173
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
174
|
+
|
175
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
176
|
+
|
177
|
+
## Contributing
|
178
|
+
|
179
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/riseshia/exwiw.
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/exwiw
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module Adapter
|
5
|
+
class Mysql2Adapter < Base
|
6
|
+
def execute(query_ast)
|
7
|
+
sql = compile_ast(query_ast)
|
8
|
+
|
9
|
+
@logger.debug(" Executing SQL: \n#{sql}")
|
10
|
+
connection.query(sql, cast: false, as: :array).to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_bulk_insert(results, table)
|
14
|
+
table_name = table.name
|
15
|
+
|
16
|
+
value_list = results.map do |row|
|
17
|
+
quoted_values = row.map do |value|
|
18
|
+
escape_value(value)
|
19
|
+
end
|
20
|
+
"(" + quoted_values.join(', ') + ")"
|
21
|
+
end
|
22
|
+
values = value_list.join(",\n")
|
23
|
+
|
24
|
+
column_names = table.columns.map(&:name).join(', ')
|
25
|
+
"INSERT INTO #{table_name} (#{column_names}) VALUES\n#{values};"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bulk_delete(select_query_ast, table)
|
29
|
+
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
30
|
+
|
31
|
+
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
32
|
+
|
33
|
+
if select_query_ast.join_clauses.empty?
|
34
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
35
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
36
|
+
compiled_where_conditions = select_query_ast.
|
37
|
+
where_clauses.
|
38
|
+
select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
|
39
|
+
map do |where|
|
40
|
+
compile_where_condition(where, select_query_ast.from_table_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
if compiled_where_conditions.size > 0
|
44
|
+
sql += "\nWHERE "
|
45
|
+
sql += compiled_where_conditions.join(' AND ')
|
46
|
+
end
|
47
|
+
sql += ";"
|
48
|
+
|
49
|
+
return sql
|
50
|
+
end
|
51
|
+
|
52
|
+
subquery_ast = Exwiw::QueryAst::Select.new
|
53
|
+
first_join = select_query_ast.join_clauses.first.clone
|
54
|
+
|
55
|
+
subquery_ast.from(first_join.join_table_name)
|
56
|
+
primay_key_col = table.columns.find { |col| col.name == table.primary_key }
|
57
|
+
subquery_ast.select([primay_key_col])
|
58
|
+
select_query_ast.join_clauses[1..].each do |join|
|
59
|
+
subquery_ast.join(join)
|
60
|
+
end
|
61
|
+
first_join.where_clauses.each do |where|
|
62
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
63
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
64
|
+
subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
|
65
|
+
end
|
66
|
+
|
67
|
+
foreign_key = first_join.foreign_key
|
68
|
+
subquery_sql = compile_ast(subquery_ast)
|
69
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql});"
|
70
|
+
|
71
|
+
sql
|
72
|
+
end
|
73
|
+
|
74
|
+
def compile_ast(query_ast)
|
75
|
+
raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select)
|
76
|
+
|
77
|
+
sql = "SELECT "
|
78
|
+
sql += query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ')
|
79
|
+
sql += " FROM #{query_ast.from_table_name}"
|
80
|
+
|
81
|
+
query_ast.join_clauses.each do |join|
|
82
|
+
sql += " JOIN #{join.join_table_name} ON #{query_ast.from_table_name}.#{join.foreign_key} = #{join.join_table_name}.#{join.primary_key}"
|
83
|
+
|
84
|
+
join.where_clauses.each do |where|
|
85
|
+
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
86
|
+
sql += " AND #{compiled_where_condition}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if query_ast.where_clauses.any?
|
91
|
+
sql += " WHERE "
|
92
|
+
sql += query_ast.where_clauses.map { |where| compile_where_condition(where, query_ast.from_table_name) }.join(' AND ')
|
93
|
+
end
|
94
|
+
|
95
|
+
sql
|
96
|
+
end
|
97
|
+
|
98
|
+
private def compile_where_condition(where_clause, table_name)
|
99
|
+
# Use as it is if it's a raw query
|
100
|
+
return where_clause if where_clause.is_a?(String)
|
101
|
+
|
102
|
+
key = "#{table_name}.#{where_clause.column_name}"
|
103
|
+
|
104
|
+
if where_clause.operator == :eq
|
105
|
+
values = where_clause.value.map { |v| escape_value(v) }
|
106
|
+
|
107
|
+
if values.size == 1
|
108
|
+
"#{key} = #{values.first}"
|
109
|
+
else
|
110
|
+
"#{key} IN (#{values.join(', ')})"
|
111
|
+
end
|
112
|
+
else
|
113
|
+
raise "Unsupported operator: #{where_clause.operator}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private def escape_value(value)
|
118
|
+
case value
|
119
|
+
when nil
|
120
|
+
"NULL"
|
121
|
+
when String
|
122
|
+
qv = escape_single_quote(value)
|
123
|
+
"'#{qv}'"
|
124
|
+
else
|
125
|
+
value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private def escape_single_quote(value)
|
130
|
+
value.gsub("'", "''")
|
131
|
+
end
|
132
|
+
|
133
|
+
private def compile_column_name(ast, column)
|
134
|
+
case column
|
135
|
+
when Exwiw::QueryAst::ColumnValue::Plain
|
136
|
+
"#{ast.from_table_name}.#{column.name}"
|
137
|
+
when Exwiw::QueryAst::ColumnValue::RawSql
|
138
|
+
column.value
|
139
|
+
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
140
|
+
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
141
|
+
if part.start_with?('{')
|
142
|
+
name = part[1..-2]
|
143
|
+
"#{ast.from_table_name}.#{name}"
|
144
|
+
else
|
145
|
+
"'#{part}'"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
replaced = parts.join(", ")
|
150
|
+
"CONCAT(#{replaced})"
|
151
|
+
else
|
152
|
+
raise "Unreachable case: #{column.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private def connection
|
157
|
+
@connection ||=
|
158
|
+
begin
|
159
|
+
require 'mysql2'
|
160
|
+
Mysql2::Client.new(
|
161
|
+
host: @connection_config.host,
|
162
|
+
port: @connection_config.port,
|
163
|
+
username: @connection_config.user,
|
164
|
+
password: @connection_config.password,
|
165
|
+
database: @connection_config.database_name
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exwiw
|
4
|
+
module Adapter
|
5
|
+
class PostgresqlAdapter < Base
|
6
|
+
def execute(query_ast)
|
7
|
+
sql = compile_ast(query_ast)
|
8
|
+
|
9
|
+
@logger.debug(" Executing SQL: \n#{sql}")
|
10
|
+
connection.exec(sql).values
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_bulk_insert(results, table)
|
14
|
+
table_name = table.name
|
15
|
+
|
16
|
+
value_list = results.map do |row|
|
17
|
+
quoted_values = row.map do |value|
|
18
|
+
escape_value(value)
|
19
|
+
end
|
20
|
+
"(" + quoted_values.join(', ') + ")"
|
21
|
+
end
|
22
|
+
values = value_list.join(",\n")
|
23
|
+
|
24
|
+
column_names = table.columns.map(&:name).join(', ')
|
25
|
+
"INSERT INTO #{table_name} (#{column_names}) VALUES\n#{values};"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bulk_delete(select_query_ast, table)
|
29
|
+
raise NotImplementedError unless select_query_ast.is_a?(Exwiw::QueryAst::Select)
|
30
|
+
|
31
|
+
sql = "DELETE FROM #{select_query_ast.from_table_name}"
|
32
|
+
|
33
|
+
if select_query_ast.join_clauses.empty?
|
34
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
35
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
36
|
+
compiled_where_conditions = select_query_ast.
|
37
|
+
where_clauses.
|
38
|
+
select { |where| where.is_a?(Exwiw::QueryAst::WhereClause) }.
|
39
|
+
map do |where|
|
40
|
+
compile_where_condition(where, select_query_ast.from_table_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
if compiled_where_conditions.size > 0
|
44
|
+
sql += "\nWHERE "
|
45
|
+
sql += compiled_where_conditions.join(' AND ')
|
46
|
+
end
|
47
|
+
sql += ";"
|
48
|
+
|
49
|
+
return sql
|
50
|
+
end
|
51
|
+
|
52
|
+
subquery_ast = Exwiw::QueryAst::Select.new
|
53
|
+
first_join = select_query_ast.join_clauses.first.clone
|
54
|
+
|
55
|
+
subquery_ast.from(first_join.join_table_name)
|
56
|
+
primay_key_col = table.columns.find { |col| col.name == table.primary_key }
|
57
|
+
subquery_ast.select([primay_key_col])
|
58
|
+
select_query_ast.join_clauses[1..].each do |join|
|
59
|
+
subquery_ast.join(join)
|
60
|
+
end
|
61
|
+
first_join.where_clauses.each do |where|
|
62
|
+
# Ignore filter option, because bulk delete is for cleaning before import,
|
63
|
+
# so it should delete all records to avoid foreign key violation & data consistancy.
|
64
|
+
subquery_ast.where(where) if where.is_a?(Exwiw::QueryAst::WhereClause)
|
65
|
+
end
|
66
|
+
|
67
|
+
foreign_key = first_join.foreign_key
|
68
|
+
subquery_sql = compile_ast(subquery_ast)
|
69
|
+
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql});"
|
70
|
+
|
71
|
+
sql
|
72
|
+
end
|
73
|
+
|
74
|
+
def compile_ast(query_ast)
|
75
|
+
raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select)
|
76
|
+
|
77
|
+
sql = "SELECT "
|
78
|
+
sql += query_ast.columns.map { |col| compile_column_name(query_ast, col) }.join(', ')
|
79
|
+
sql += " FROM #{query_ast.from_table_name}"
|
80
|
+
|
81
|
+
query_ast.join_clauses.each do |join|
|
82
|
+
sql += " JOIN #{join.join_table_name} ON #{query_ast.from_table_name}.#{join.foreign_key} = #{join.join_table_name}.#{join.primary_key}"
|
83
|
+
|
84
|
+
join.where_clauses.each do |where|
|
85
|
+
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
86
|
+
sql += " AND #{compiled_where_condition}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if query_ast.where_clauses.any?
|
91
|
+
sql += " WHERE "
|
92
|
+
sql += query_ast.where_clauses.map { |where| compile_where_condition(where, query_ast.from_table_name) }.join(' AND ')
|
93
|
+
end
|
94
|
+
|
95
|
+
sql
|
96
|
+
end
|
97
|
+
|
98
|
+
private def compile_where_condition(where_clause, table_name)
|
99
|
+
# Use as it is if it's a raw query
|
100
|
+
return where_clause if where_clause.is_a?(String)
|
101
|
+
|
102
|
+
key = "#{table_name}.#{where_clause.column_name}"
|
103
|
+
|
104
|
+
if where_clause.operator == :eq
|
105
|
+
values = where_clause.value.map { |v| escape_value(v) }
|
106
|
+
|
107
|
+
if values.size == 1
|
108
|
+
"#{key} = #{values.first}"
|
109
|
+
else
|
110
|
+
"#{key} IN (#{values.join(', ')})"
|
111
|
+
end
|
112
|
+
else
|
113
|
+
raise "Unsupported operator: #{where_clause.operator}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private def escape_value(value)
|
118
|
+
case value
|
119
|
+
when nil
|
120
|
+
"NULL"
|
121
|
+
when String
|
122
|
+
qv = escape_single_quote(value)
|
123
|
+
"'#{qv}'"
|
124
|
+
else
|
125
|
+
value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private def escape_single_quote(value)
|
130
|
+
value.gsub("'", "''")
|
131
|
+
end
|
132
|
+
|
133
|
+
private def compile_column_name(ast, column)
|
134
|
+
case column
|
135
|
+
when Exwiw::QueryAst::ColumnValue::Plain
|
136
|
+
"#{ast.from_table_name}.#{column.name}"
|
137
|
+
when Exwiw::QueryAst::ColumnValue::RawSql
|
138
|
+
column.value
|
139
|
+
when Exwiw::QueryAst::ColumnValue::ReplaceWith
|
140
|
+
parts = column.value.scan(/[^{}]+|\{[^{}]*\}/).map do |part|
|
141
|
+
if part.start_with?('{')
|
142
|
+
name = part[1..-2]
|
143
|
+
"#{ast.from_table_name}.#{name}"
|
144
|
+
else
|
145
|
+
"'#{part}'"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
replaced = parts.join(", ")
|
150
|
+
"CONCAT(#{replaced})"
|
151
|
+
else
|
152
|
+
raise "Unreachable case: #{column.inspect}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private def connection
|
157
|
+
@connection ||=
|
158
|
+
begin
|
159
|
+
require 'pg'
|
160
|
+
PG.connect(
|
161
|
+
host: @connection_config.host,
|
162
|
+
port: @connection_config.port,
|
163
|
+
user: @connection_config.user,
|
164
|
+
password: @connection_config.password,
|
165
|
+
dbname: @connection_config.database_name
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|