torque-postgresql 4.0.0.rc1 → 4.0.0
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.
- checksums.yaml +4 -4
- data/lib/generators/torque/function_generator.rb +13 -0
- data/lib/generators/torque/templates/function.sql.erb +4 -0
- data/lib/generators/torque/templates/type.sql.erb +2 -0
- data/lib/generators/torque/templates/view.sql.erb +3 -0
- data/lib/generators/torque/type_generator.rb +13 -0
- data/lib/generators/torque/view_generator.rb +16 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +48 -10
- data/lib/torque/postgresql/adapter/schema_definitions.rb +22 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +47 -1
- data/lib/torque/postgresql/adapter/schema_statements.rb +45 -0
- data/lib/torque/postgresql/arel/nodes.rb +14 -0
- data/lib/torque/postgresql/arel/visitors.rb +4 -0
- data/lib/torque/postgresql/attributes/builder/full_text_search.rb +16 -28
- data/lib/torque/postgresql/base.rb +2 -1
- data/lib/torque/postgresql/config.rb +35 -1
- data/lib/torque/postgresql/function.rb +33 -0
- data/lib/torque/postgresql/railtie.rb +26 -1
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
- data/lib/torque/postgresql/relation/buckets.rb +124 -0
- data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
- data/lib/torque/postgresql/relation/inheritance.rb +18 -8
- data/lib/torque/postgresql/relation/join_series.rb +112 -0
- data/lib/torque/postgresql/relation/merger.rb +17 -3
- data/lib/torque/postgresql/relation.rb +18 -28
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
- data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
- data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
- data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
- data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
- data/lib/torque/postgresql/versioned_commands.rb +161 -0
- data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
- data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
- data/spec/initialize.rb +9 -0
- data/spec/schema.rb +2 -4
- data/spec/spec_helper.rb +6 -1
- data/spec/tests/full_text_seach_test.rb +30 -2
- data/spec/tests/relation_spec.rb +229 -0
- data/spec/tests/schema_spec.rb +4 -1
- data/spec/tests/versioned_commands_spec.rb +513 -0
- metadata +33 -3
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
class IllegalCommandTypeError < ActiveRecord::MigrationError
|
6
|
+
def initialize(file)
|
7
|
+
super(<<~MSG.squish)
|
8
|
+
Illegal name for command file '#{file}'. Commands are more strict and require
|
9
|
+
the version, one of create, update, or remove, type of object, name
|
10
|
+
and operation version to be present in the filename.
|
11
|
+
(e.g. 20250101010101_create_function_my_function_v1.sql)
|
12
|
+
MSG
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module VersionedCommands
|
17
|
+
module MigrationContext
|
18
|
+
InvalidMigrationTimestampError = ActiveRecord::InvalidMigrationTimestampError
|
19
|
+
PGAdapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
20
|
+
|
21
|
+
def migrations
|
22
|
+
return super unless running_for_pg?
|
23
|
+
|
24
|
+
commands = command_files.map do |file|
|
25
|
+
version, op, type, name, op_version, scope = parse_command_filename(file)
|
26
|
+
raise IllegalCommandTypeError.new(file) unless version
|
27
|
+
if validate_timestamp? && !valid_migration_timestamp?(version)
|
28
|
+
raise InvalidMigrationTimestampError.new(version, [op, type, name, op_version].join('_'))
|
29
|
+
end
|
30
|
+
|
31
|
+
version = version.to_i
|
32
|
+
CommandMigration.new(file, version, op, type, name, op_version.to_i, scope)
|
33
|
+
end
|
34
|
+
|
35
|
+
super.concat(commands).sort_by(&:version)
|
36
|
+
end
|
37
|
+
|
38
|
+
def migrations_status
|
39
|
+
return super unless running_for_pg?
|
40
|
+
db_list = schema_migration.normalized_versions
|
41
|
+
|
42
|
+
commands = command_files.map do |file|
|
43
|
+
version, op, type, name, op_version, scope = parse_command_filename(file)
|
44
|
+
raise IllegalCommandTypeError.new(file) unless version
|
45
|
+
if validate_timestamp? && !valid_migration_timestamp?(version)
|
46
|
+
raise InvalidMigrationTimestampError.new(version, [op, type, name, op_version].join('_'))
|
47
|
+
end
|
48
|
+
|
49
|
+
version = schema_migration.normalize_migration_number(version)
|
50
|
+
status = db_list.delete(version) ? "up" : "down"
|
51
|
+
[status, version, "#{op.capitalize} #{type.capitalize} #{name}#{scope} (v#{op_version})"]
|
52
|
+
end
|
53
|
+
|
54
|
+
(commands + super).uniq(&:second).sort_by(&:second)
|
55
|
+
end
|
56
|
+
|
57
|
+
def migration_commands
|
58
|
+
migrations.select { |m| m.is_a?(VersionedCommands::CommandMigration) }
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Checks if the current migration context is running for PostgreSQL
|
64
|
+
def running_for_pg?
|
65
|
+
connection_pool.db_config.adapter_class <= PGAdapter
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get the list of all versioned command files
|
69
|
+
def command_files
|
70
|
+
paths = Array(migrations_paths)
|
71
|
+
Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.sql" }]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Commands are more strict with the filename format
|
75
|
+
def parse_command_filename(filename)
|
76
|
+
File.basename(filename).scan(VersionedCommands.filename_regexp).first
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
ActiveRecord::MigrationContext.prepend(MigrationContext)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module VersionedCommands
|
6
|
+
module Migrator
|
7
|
+
def execute_migration_in_transaction(migration)
|
8
|
+
@versioned_command = versioned_command?(migration) && migration
|
9
|
+
super
|
10
|
+
ensure
|
11
|
+
@versioned_command = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_version_state_after_migrating(version)
|
15
|
+
return super if (command = @versioned_command) == false
|
16
|
+
|
17
|
+
@versioned_table ||= VersionedCommands::SchemaTable.new(connection.pool)
|
18
|
+
@versioned_counter ||= @versioned_table.count
|
19
|
+
|
20
|
+
if down?
|
21
|
+
@versioned_counter -= 1
|
22
|
+
@versioned_table.delete_version(command)
|
23
|
+
@versioned_table.drop_table if @versioned_counter.zero?
|
24
|
+
else
|
25
|
+
@versioned_table.create_table if @versioned_counter.zero?
|
26
|
+
@versioned_table.create_version(command)
|
27
|
+
@versioned_counter += 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def versioned_command?(migration)
|
32
|
+
migration.is_a?(VersionedCommands::CommandMigration)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveRecord::Migrator.prepend(Migrator)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module VersionedCommands
|
6
|
+
class SchemaTable
|
7
|
+
attr_reader :arel_table
|
8
|
+
|
9
|
+
def initialize(pool)
|
10
|
+
@pool = pool
|
11
|
+
@arel_table = ::Arel::Table.new(table_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_version(command)
|
15
|
+
im = ::Arel::InsertManager.new(arel_table)
|
16
|
+
im.insert(
|
17
|
+
arel_table[primary_key] => command.version,
|
18
|
+
arel_table['type'] => command.type,
|
19
|
+
arel_table['object_name'] => command.object_name,
|
20
|
+
)
|
21
|
+
|
22
|
+
@pool.with_connection do |connection|
|
23
|
+
connection.insert(im, "#{name} Create", primary_key, command.version)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_version(command)
|
28
|
+
dm = ::Arel::DeleteManager.new(arel_table)
|
29
|
+
dm.wheres = [arel_table[primary_key].eq(command.version.to_s)]
|
30
|
+
|
31
|
+
@pool.with_connection do |connection|
|
32
|
+
connection.delete(dm, "#{name} Destroy")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def primary_key
|
37
|
+
'version'
|
38
|
+
end
|
39
|
+
|
40
|
+
def name
|
41
|
+
'Torque::PostgreSQL::VersionedCommand'
|
42
|
+
end
|
43
|
+
|
44
|
+
def table_name
|
45
|
+
[
|
46
|
+
ActiveRecord::Base.table_name_prefix,
|
47
|
+
PostgreSQL.config.versioned_commands.table_name,
|
48
|
+
ActiveRecord::Base.table_name_suffix,
|
49
|
+
].join
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_table
|
53
|
+
@pool.with_connection do |connection|
|
54
|
+
return if connection.table_exists?(table_name)
|
55
|
+
|
56
|
+
parent = @pool.schema_migration.table_name
|
57
|
+
connection.create_table(table_name, inherits: parent) do |t|
|
58
|
+
t.string :type, null: false, index: true
|
59
|
+
t.string :object_name, null: false, index: true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def drop_table
|
65
|
+
@pool.with_connection do |connection|
|
66
|
+
connection.drop_table table_name, if_exists: true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def count
|
71
|
+
return 0 unless table_exists?
|
72
|
+
|
73
|
+
sm = ::Arel::SelectManager.new(arel_table)
|
74
|
+
sm.project(*FN.count(::Arel.star))
|
75
|
+
|
76
|
+
@pool.with_connection do |connection|
|
77
|
+
connection.select_value(sm, "#{self.class} Count")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def table_exists?
|
82
|
+
@pool.with_connection { |connection| connection.data_source_exists?(table_name) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def versions_of(type)
|
86
|
+
return [] unless table_exists?
|
87
|
+
|
88
|
+
sm = ::Arel::SelectManager.new(arel_table)
|
89
|
+
sm.project(arel_table['object_name'], FN.count(::Arel.star).as('version'))
|
90
|
+
sm.where(arel_table['type'].eq(type.to_s))
|
91
|
+
sm.group(arel_table['object_name'])
|
92
|
+
sm.order(arel_table['object_name'].asc)
|
93
|
+
|
94
|
+
@pool.with_connection do |connection|
|
95
|
+
connection.select_rows(sm, "#{name} Load")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'versioned_commands/command_migration'
|
4
|
+
require_relative 'versioned_commands/migration_context'
|
5
|
+
require_relative 'versioned_commands/migrator'
|
6
|
+
require_relative 'versioned_commands/schema_table'
|
7
|
+
|
8
|
+
module Torque
|
9
|
+
module PostgreSQL
|
10
|
+
# Takes advantage of Rails migrations to create other sorts of
|
11
|
+
# objects/commands that can also be versioned. Everything migrated will
|
12
|
+
# still live within Migrations borders (i.e., the schema_migrations), but
|
13
|
+
# the way they are handled and registered in the schema dumper is completely
|
14
|
+
# different
|
15
|
+
module VersionedCommands
|
16
|
+
RAILS_APP = defined?(Rails.application.paths)
|
17
|
+
NAME_MATCH = '"?((?:[_a-z0-9]+"?\."?)?[_a-z0-9]+)"?'
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Check if the type is current enabled
|
21
|
+
def valid_type?(type)
|
22
|
+
PostgreSQL.config.versioned_commands.types.include?(type.to_sym)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run the internal validations for the given type and content
|
26
|
+
def validate!(type, content, name)
|
27
|
+
method_name = :"validate_#{type}!"
|
28
|
+
return send(method_name, content, name) if valid_type?(type)
|
29
|
+
raise ArgumentError, "Unknown versioned command type: #{type}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the content of the command based on the type, name, and version
|
33
|
+
def fetch_command(dirs, type, name, version)
|
34
|
+
paths = Array.wrap(dirs).map { |d| "#{d}/**/*_#{type}_#{name}_v#{version}.sql" }
|
35
|
+
files = Dir[*paths]
|
36
|
+
return File.read(files.first) if files.one?
|
37
|
+
|
38
|
+
raise ArgumentError, <<~MSG.squish if files.none?
|
39
|
+
No previous version found for #{type} #{name}
|
40
|
+
of version v#{version}.
|
41
|
+
MSG
|
42
|
+
|
43
|
+
raise ArgumentError, <<~MSG.squish if files.many?
|
44
|
+
Multiple files found for #{type} #{name}
|
45
|
+
of version v#{version}.
|
46
|
+
MSG
|
47
|
+
end
|
48
|
+
|
49
|
+
# The regexp is dynamic due to the list of available types
|
50
|
+
def filename_regexp
|
51
|
+
@filename_regexp ||= begin
|
52
|
+
types = PostgreSQL.config.versioned_commands.types
|
53
|
+
Regexp.new([
|
54
|
+
"\\A([0-9]+)_",
|
55
|
+
"(create|update|remove)_",
|
56
|
+
"(#{types.join('|')})_",
|
57
|
+
"([_a-z0-9]*)",
|
58
|
+
"_v([0-9]+)",
|
59
|
+
"\\.?([_a-z0-9]*)?",
|
60
|
+
"\\.sql\\z",
|
61
|
+
].join)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Validate that the content of the command is correct
|
68
|
+
def validate_function!(content, name)
|
69
|
+
result = content.scan(Regexp.new([
|
70
|
+
'^\s*CREATE\s+(OR\s+REPLACE)?\s*',
|
71
|
+
"FUNCTION\\s+#{NAME_MATCH}",
|
72
|
+
].join, 'mi'))
|
73
|
+
|
74
|
+
names = result.map(&:last).compact.uniq(&:downcase)
|
75
|
+
raise ArgumentError, <<~MSG.squish if names.size > 1
|
76
|
+
Multiple functions definition found.
|
77
|
+
MSG
|
78
|
+
|
79
|
+
raise ArgumentError, <<~MSG.squish unless result.all?(&:first)
|
80
|
+
'OR REPLACE' is required for proper migration support.
|
81
|
+
MSG
|
82
|
+
|
83
|
+
fn_name = names.first.downcase.sub('.', '_')
|
84
|
+
raise ArgumentError, <<~MSG.squish if fn_name != name.downcase
|
85
|
+
Function name must match file name.
|
86
|
+
MSG
|
87
|
+
end
|
88
|
+
|
89
|
+
# Validate that the content of the command is correct
|
90
|
+
def validate_type!(content, name)
|
91
|
+
creates = content.scan(Regexp.new(['^\s*CREATE\s+TYPE\s+', NAME_MATCH].join, 'mi'))
|
92
|
+
drops = content.scan(Regexp.new([
|
93
|
+
'^\s*DROP\s+TYPE\s+(IF\s+EXISTS)?\s*',
|
94
|
+
NAME_MATCH,
|
95
|
+
].join, 'mi'))
|
96
|
+
|
97
|
+
raise ArgumentError, <<~MSG.squish if creates.size > 1
|
98
|
+
More than one type definition found.
|
99
|
+
MSG
|
100
|
+
|
101
|
+
raise ArgumentError, <<~MSG.squish if drops.size > 1
|
102
|
+
More than one type drop found.
|
103
|
+
MSG
|
104
|
+
|
105
|
+
raise ArgumentError, <<~MSG.squish if drops.empty?
|
106
|
+
'DROP TYPE' is required for proper migration support.
|
107
|
+
MSG
|
108
|
+
|
109
|
+
create_name = creates.first.last.downcase
|
110
|
+
raise ArgumentError, <<~MSG.squish if drops.first.last.downcase != create_name
|
111
|
+
Drop does not match create.
|
112
|
+
MSG
|
113
|
+
|
114
|
+
create_name = create_name.sub('.', '_')
|
115
|
+
raise ArgumentError, <<~MSG.squish if create_name != name.downcase
|
116
|
+
Type name must match file name.
|
117
|
+
MSG
|
118
|
+
end
|
119
|
+
|
120
|
+
# Validate that the content of the command is correct
|
121
|
+
def validate_view!(content, name)
|
122
|
+
result = content.scan(Regexp.new([
|
123
|
+
'^\s*CREATE\s+(OR\s+REPLACE)?\s*',
|
124
|
+
'((?:TEMP|TEMPORARY|MATERIALIZED)\s+)?',
|
125
|
+
'(?:RECURSIVE\s+)?',
|
126
|
+
"VIEW\\s+#{NAME_MATCH}",
|
127
|
+
].join, 'mi'))
|
128
|
+
|
129
|
+
raise ArgumentError, <<~MSG.squish if result.empty?
|
130
|
+
Missing or invalid view definition.
|
131
|
+
MSG
|
132
|
+
|
133
|
+
raise ArgumentError, <<~MSG.squish if result.size > 1
|
134
|
+
More than one view definition found.
|
135
|
+
MSG
|
136
|
+
|
137
|
+
with_replace, opt, view_name = result.first
|
138
|
+
if opt&.strip == 'MATERIALIZED'
|
139
|
+
raise ArgumentError, <<~MSG.squish if with_replace.present?
|
140
|
+
Materialized view does not support 'OR REPLACE'.
|
141
|
+
MSG
|
142
|
+
|
143
|
+
with_drop = "DROP MATERIALIZED VIEW IF EXISTS #{view_name};"
|
144
|
+
raise ArgumentError, <<~MSG.squish unless content.include?(with_drop)
|
145
|
+
'DROP MATERIALIZED VIEW IF EXISTS' is required for proper migration support.
|
146
|
+
MSG
|
147
|
+
else
|
148
|
+
raise ArgumentError, <<~MSG.squish if with_replace.blank?
|
149
|
+
'OR REPLACE' is required for proper migration support.
|
150
|
+
MSG
|
151
|
+
end
|
152
|
+
|
153
|
+
view_name = view_name.downcase.sub('.', '_')
|
154
|
+
raise ArgumentError, <<~MSG.squish if view_name != name.downcase
|
155
|
+
View name must match file name.
|
156
|
+
MSG
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/spec/initialize.rb
CHANGED
@@ -16,6 +16,10 @@ require_relative '../lib/torque/postgresql/attributes/period'
|
|
16
16
|
require_relative '../lib/torque/postgresql/attributes/full_text_search'
|
17
17
|
|
18
18
|
require_relative '../lib/torque/postgresql/relation/auxiliary_statement'
|
19
|
+
require_relative '../lib/torque/postgresql/relation/join_series'
|
20
|
+
require_relative '../lib/torque/postgresql/relation/buckets'
|
21
|
+
|
22
|
+
require_relative '../lib/torque/postgresql/versioned_commands'
|
19
23
|
|
20
24
|
module Torque
|
21
25
|
module PostgreSQL
|
@@ -27,6 +31,11 @@ module Torque
|
|
27
31
|
Attributes::FullTextSearch.include_on(ActiveRecord::Base)
|
28
32
|
|
29
33
|
Relation.include(Relation::AuxiliaryStatement)
|
34
|
+
Relation.include(Relation::JoinSeries)
|
35
|
+
Relation.include(Relation::Buckets)
|
36
|
+
|
37
|
+
config.versioned_commands.enabled = true
|
38
|
+
ActiveRecord::Schema::Definition.include(Adapter::Definition)
|
30
39
|
|
31
40
|
::Object.const_set('TorqueCTE', AuxiliaryStatement)
|
32
41
|
::Object.const_set('TorqueRecursiveCTE', AuxiliaryStatement::Recursive)
|
data/spec/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
version =
|
13
|
+
version = 6
|
14
14
|
|
15
15
|
return if ActiveRecord::Migrator.current_version == version
|
16
16
|
ActiveRecord::Schema.define(version: version) do
|
@@ -127,6 +127,7 @@ ActiveRecord::Schema.define(version: version) do
|
|
127
127
|
create_table "users", force: :cascade do |t|
|
128
128
|
t.string "name", null: false
|
129
129
|
t.enum "role", enum_type: :roles, default: :visitor
|
130
|
+
t.integer "age"
|
130
131
|
t.datetime "created_at", null: false
|
131
132
|
t.datetime "updated_at", null: false
|
132
133
|
end
|
@@ -176,9 +177,6 @@ ActiveRecord::Schema.define(version: version) do
|
|
176
177
|
# create_table "activity_images", force: :cascade, inherits: [:activities, :images]
|
177
178
|
|
178
179
|
add_foreign_key "posts", "authors"
|
179
|
-
rescue Exception => e
|
180
|
-
byebug
|
181
|
-
raise
|
182
180
|
end
|
183
181
|
|
184
182
|
ActiveRecord::Base.connection.schema_cache.clear!
|
data/spec/spec_helper.rb
CHANGED
@@ -215,14 +215,30 @@ RSpec.describe 'FullTextSearch' do
|
|
215
215
|
expect(result.to_sql).to eql(parts)
|
216
216
|
end
|
217
217
|
|
218
|
-
it 'can use
|
219
|
-
result = Course.full_text_search('test',
|
218
|
+
it 'can use default query mode' do
|
219
|
+
result = Course.full_text_search('test', mode: :default)
|
220
220
|
parts = 'SELECT "courses".* FROM "courses"'
|
221
221
|
parts << ' WHERE "courses"."search_vector" @@'
|
222
222
|
parts << " TO_TSQUERY('english', 'test')"
|
223
223
|
expect(result.to_sql).to eql(parts)
|
224
224
|
end
|
225
225
|
|
226
|
+
it 'can use plain query mode' do
|
227
|
+
result = Course.full_text_search('test', mode: :plain)
|
228
|
+
parts = 'SELECT "courses".* FROM "courses"'
|
229
|
+
parts << ' WHERE "courses"."search_vector" @@'
|
230
|
+
parts << " PLAINTO_TSQUERY('english', 'test')"
|
231
|
+
expect(result.to_sql).to eql(parts)
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'can use web query mode' do
|
235
|
+
result = Course.full_text_search('test', mode: :web)
|
236
|
+
parts = 'SELECT "courses".* FROM "courses"'
|
237
|
+
parts << ' WHERE "courses"."search_vector" @@'
|
238
|
+
parts << " WEBSEARCH_TO_TSQUERY('english', 'test')"
|
239
|
+
expect(result.to_sql).to eql(parts)
|
240
|
+
end
|
241
|
+
|
226
242
|
it 'can use a attribute as the language' do
|
227
243
|
result = Course.full_text_search('test', language: :lang)
|
228
244
|
parts = 'SELECT "courses".* FROM "courses"'
|
@@ -248,5 +264,17 @@ RSpec.describe 'FullTextSearch' do
|
|
248
264
|
expect(binds.first.value).to eq('english')
|
249
265
|
expect(binds.second.value).to eq('test')
|
250
266
|
end
|
267
|
+
|
268
|
+
it 'raises an error when the language is not found' do
|
269
|
+
expect do
|
270
|
+
Course.full_text_search('test', language: '')
|
271
|
+
end.to raise_error(ArgumentError, /Unable to determine language/)
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'raises an error when the mode is invalid' do
|
275
|
+
expect do
|
276
|
+
Course.full_text_search('test', mode: :invalid)
|
277
|
+
end.to raise_error(ArgumentError, /Invalid mode :invalid for full text search/)
|
278
|
+
end
|
251
279
|
end
|
252
280
|
end
|