sqlui 0.1.40 → 0.1.41

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69e4dde0aaa2c01991a06d6ac073494c10a22b6f55b4f40d0a7dedc5f5ab2e7a
4
- data.tar.gz: 5321247d38c5845e5398b8cb6560e6581a389bac11cf9ea06e5ace893c7f0b5d
3
+ metadata.gz: a4307dfb13604d60040248f4a4135756f52310be559dfd3a65778e8400ab5454
4
+ data.tar.gz: 00ea3b80ba354e767521943cf83058cceebd53d5d3f112fdabc09952ee375471
5
5
  SHA512:
6
- metadata.gz: 032dc0a5c4a53e8273b2cf9f2916dd3d322d2c10c2e3ccf9a067471cb029bdeed34c22889071220da9d91f83e4e3976c90ef2eeb8327c84a0de88675e60e9f33
7
- data.tar.gz: 7a39174c4c5b2b19cd0adf03a51a522c40b577cac9cd16bfcce1cd32ede24816aa292cc8388d960172853c06420b56bba8193320650c5fa8bf26b40ebdabe3df
6
+ metadata.gz: 1aab467177964e30d47233e5f8e60922732de9cdb0238104dc7b74eecf9271ce80e3ac449c3afaac5c4ea91863160cd7edbf352b88cc8ae11274b738abe60271
7
+ data.tar.gz: 2d644c6ecd81b489acad2986be0d60f0dee3c20e5165943938279148af8f4f9f7a923a410bed570be71aad99b3f591ff92c050cd2a27df9ae06bdbb4b63ef8d7
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.40
1
+ 0.1.41
data/app/args.rb CHANGED
@@ -24,6 +24,10 @@ class Args
24
24
  fetch_optional(hash, key, Hash)
25
25
  end
26
26
 
27
+ def self.fetch_optional_array(hash, key)
28
+ fetch_optional(hash, key, Array)
29
+ end
30
+
27
31
  def self.fetch_non_nil(hash, key, *classes)
28
32
  raise ArgumentError, "required parameter #{key} missing" unless hash.key?(key)
29
33
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'mysql2'
4
5
  require 'set'
5
6
 
@@ -7,7 +8,7 @@ require_relative 'args'
7
8
 
8
9
  # Config for a single database.
9
10
  class DatabaseConfig
10
- attr_reader :display_name, :description, :url_path, :saved_path, :table_aliases, :client_params
11
+ attr_reader :display_name, :description, :url_path, :joins, :saved_path, :table_aliases, :client_params
11
12
 
12
13
  def initialize(hash)
13
14
  @display_name = Args.fetch_non_empty_string(hash, :display_name).strip
@@ -17,6 +18,15 @@ class DatabaseConfig
17
18
  raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
18
19
 
19
20
  @saved_path = Args.fetch_non_empty_string(hash, :saved_path).strip
21
+ @joins = Args.fetch_optional_array(hash, :joins) || []
22
+ @joins.map do |join|
23
+ next if join.is_a?(Hash) &&
24
+ join.keys.size == 2 &&
25
+ join[:label].is_a?(String) && !join[:label].strip.empty? &&
26
+ join[:apply].is_a?(String) && !join[:apply].strip.empty?
27
+
28
+ raise ArgumentError, "invalid join #{join.to_json}"
29
+ end
20
30
  @table_aliases = Args.fetch_optional_hash(hash, :table_aliases) || {}
21
31
  @table_aliases = @table_aliases.each do |table, a|
22
32
  raise ArgumentError, "invalid alias for table #{table} (#{a}), expected string" unless a.is_a?(String)
@@ -38,18 +38,17 @@ class DatabaseMetadata
38
38
  extra
39
39
  from information_schema.columns
40
40
  #{where_clause}
41
- order by table_schema, table_name, column_name, ordinal_position;
41
+ order by table_schema, table_name, ordinal_position;
42
42
  SQL
43
43
  )
44
- column_result.each do |row|
45
- row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
46
- table_schema = row[:table_schema]
44
+ column_result.to_a.each do |row|
45
+ table_schema = row.shift
47
46
  unless result[table_schema]
48
47
  result[table_schema] = {
49
48
  tables: {}
50
49
  }
51
50
  end
52
- table_name = row[:table_name]
51
+ table_name = row.shift
53
52
  tables = result[table_schema][:tables]
54
53
  unless tables[table_name]
55
54
  tables[table_name] = {
@@ -58,16 +57,16 @@ class DatabaseMetadata
58
57
  }
59
58
  end
60
59
  columns = result[table_schema][:tables][table_name][:columns]
61
- column_name = row[:column_name]
60
+ column_name = row.shift
62
61
  columns[column_name] = {} unless columns[column_name]
63
62
  column = columns[column_name]
64
63
  column[:name] = column_name
65
- column[:data_type] = row[:data_type]
66
- column[:length] = row[:character_maximum_length]
67
- column[:allow_null] = row[:is_nullable]
68
- column[:key] = row[:column_key]
69
- column[:default] = row[:column_default]
70
- column[:extra] = row[:extra]
64
+ column[:data_type] = row.shift
65
+ column[:length] = row.shift
66
+ column[:allow_null] = row.shift
67
+ column[:key] = row.shift
68
+ column[:default] = row.shift
69
+ column[:extra] = row.shift
71
70
  end
72
71
  result
73
72
  end
@@ -86,30 +85,29 @@ class DatabaseMetadata
86
85
  table_schema,
87
86
  table_name,
88
87
  index_name,
88
+ column_name,
89
89
  seq_in_index,
90
- non_unique,
91
- column_name
90
+ non_unique
92
91
  from information_schema.statistics
93
92
  #{where_clause}
94
93
  order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
95
94
  SQL
96
95
  )
97
96
  stats_result.each do |row|
98
- row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
99
- table_schema = row[:table_schema]
97
+ table_schema = row.shift
100
98
  tables = result[table_schema][:tables]
101
- table_name = row[:table_name]
99
+ table_name = row.shift
102
100
  indexes = tables[table_name][:indexes]
103
- index_name = row[:index_name]
101
+ index_name = row.shift
104
102
  indexes[index_name] = {} unless indexes[index_name]
105
103
  index = indexes[index_name]
106
- column_name = row[:column_name]
104
+ column_name = row.shift
107
105
  index[column_name] = {}
108
106
  column = index[column_name]
109
107
  column[:name] = index_name
110
- column[:seq_in_index] = row[:seq_in_index]
111
- column[:non_unique] = row[:non_unique]
112
- column[:column_name] = row[:column_name]
108
+ column[:seq_in_index] = row.shift
109
+ column[:non_unique] = row.shift
110
+ column[:column_name] = column_name
113
111
  end
114
112
  end
115
113
  end
data/app/server.rb CHANGED
@@ -12,6 +12,11 @@ require_relative 'sqlui'
12
12
  # SQLUI Sinatra server.
13
13
  class Server < Sinatra::Base
14
14
  def self.init_and_run(config, resources_dir)
15
+ Mysql2::Client.default_query_options[:as] = :array
16
+ Mysql2::Client.default_query_options[:cast_booleans] = true
17
+ Mysql2::Client.default_query_options[:database_timezone] = :utc
18
+ Mysql2::Client.default_query_options[:cache_rows] = false
19
+
15
20
  set :logging, true
16
21
  set :bind, '0.0.0.0'
17
22
  set :port, config.port
@@ -58,6 +63,7 @@ class Server < Sinatra::Base
58
63
  list_url_path: config.list_url_path,
59
64
  schemas: DatabaseMetadata.lookup(client, database),
60
65
  table_aliases: database.table_aliases,
66
+ joins: database.joins,
61
67
  saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
62
68
  contents = File.read(path)
63
69
  comment_lines = contents.split("\n").take_while do |l|
@@ -164,8 +170,8 @@ class Server < Sinatra::Base
164
170
  # get a seg fault. Seems to be a bug in Mysql2.
165
171
  if result
166
172
  column_types = MysqlTypes.map_to_google_charts_types(result.field_types)
167
- rows = result.map(&:values)
168
- columns = result.first&.keys || []
173
+ rows = result.to_a
174
+ columns = result.fields
169
175
  else
170
176
  column_types = []
171
177
  rows = []
data/app/sqlui_config.rb CHANGED
@@ -11,7 +11,7 @@ class SqluiConfig
11
11
  attr_reader :name, :port, :environment, :list_url_path, :database_configs
12
12
 
13
13
  def initialize(filename, overrides = {})
14
- config = YAML.safe_load(ERB.new(File.read(filename)).result).deep_merge!(overrides)
14
+ config = YAML.safe_load(ERB.new(File.read(filename)).result, aliases: true).deep_merge!(overrides)
15
15
  config.deep_symbolize_keys!
16
16
  @name = Args.fetch_non_empty_string(config, :name).strip
17
17
  @port = Args.fetch_non_empty_int(config, :port)
@@ -3359,23 +3359,24 @@
3359
3359
  */
3360
3360
  minPointSize = -1) {
3361
3361
  let cursor = new SpanCursor(sets, null, minPointSize).goto(from), pos = from;
3362
- let open = cursor.openStart;
3362
+ let openRanges = cursor.openStart;
3363
3363
  for (;;) {
3364
3364
  let curTo = Math.min(cursor.to, to);
3365
3365
  if (cursor.point) {
3366
- iterator.point(pos, curTo, cursor.point, cursor.activeForPoint(cursor.to), open, cursor.pointRank);
3367
- open = cursor.openEnd(curTo) + (cursor.to > curTo ? 1 : 0);
3366
+ let active = cursor.activeForPoint(cursor.to);
3367
+ let openCount = cursor.pointFrom < from ? active.length + 1 : Math.min(active.length, openRanges);
3368
+ iterator.point(pos, curTo, cursor.point, active, openCount, cursor.pointRank);
3369
+ openRanges = Math.min(cursor.openEnd(curTo), active.length);
3368
3370
  }
3369
3371
  else if (curTo > pos) {
3370
- iterator.span(pos, curTo, cursor.active, open);
3371
- open = cursor.openEnd(curTo);
3372
+ iterator.span(pos, curTo, cursor.active, openRanges);
3373
+ openRanges = cursor.openEnd(curTo);
3372
3374
  }
3373
3375
  if (cursor.to > to)
3374
- break;
3376
+ return openRanges + (cursor.point && cursor.to > to ? 1 : 0);
3375
3377
  pos = cursor.to;
3376
3378
  cursor.next();
3377
3379
  }
3378
- return open;
3379
3380
  }
3380
3381
  /**
3381
3382
  Create a range set for the given range or array of ranges. By
@@ -3679,6 +3680,8 @@
3679
3680
  this.pointRank = 0;
3680
3681
  this.to = -1000000000 /* C.Far */;
3681
3682
  this.endSide = 0;
3683
+ // The amount of open active ranges at the start of the iterator.
3684
+ // Not including points.
3682
3685
  this.openStart = -1;
3683
3686
  this.cursor = HeapCursor.from(sets, skip, minPoint);
3684
3687
  }
@@ -3719,7 +3722,7 @@
3719
3722
  next() {
3720
3723
  let from = this.to, wasPoint = this.point;
3721
3724
  this.point = null;
3722
- let trackOpen = this.openStart < 0 ? [] : null, trackExtra = 0;
3725
+ let trackOpen = this.openStart < 0 ? [] : null;
3723
3726
  for (;;) {
3724
3727
  let a = this.minActive;
3725
3728
  if (a > -1 && (this.activeTo[a] - this.cursor.from || this.active[a].endSide - this.cursor.startSide) < 0) {
@@ -3745,8 +3748,6 @@
3745
3748
  let nextVal = this.cursor.value;
3746
3749
  if (!nextVal.point) { // Opening a range
3747
3750
  this.addActive(trackOpen);
3748
- if (this.cursor.from < from && this.cursor.to > from)
3749
- trackExtra++;
3750
3751
  this.cursor.next();
3751
3752
  }
3752
3753
  else if (wasPoint && this.cursor.to == this.to && this.cursor.from < this.cursor.to) {
@@ -3759,8 +3760,6 @@
3759
3760
  this.pointRank = this.cursor.rank;
3760
3761
  this.to = this.cursor.to;
3761
3762
  this.endSide = nextVal.endSide;
3762
- if (this.cursor.from < from)
3763
- trackExtra = 1;
3764
3763
  this.cursor.next();
3765
3764
  this.forward(this.to, this.endSide);
3766
3765
  break;
@@ -3768,10 +3767,9 @@
3768
3767
  }
3769
3768
  }
3770
3769
  if (trackOpen) {
3771
- let openStart = 0;
3772
- while (openStart < trackOpen.length && trackOpen[openStart] < from)
3773
- openStart++;
3774
- this.openStart = openStart + trackExtra;
3770
+ this.openStart = 0;
3771
+ for (let i = trackOpen.length - 1; i >= 0 && trackOpen[i] < from; i--)
3772
+ this.openStart++;
3775
3773
  }
3776
3774
  }
3777
3775
  activeForPoint(to) {
@@ -5826,7 +5824,7 @@
5826
5824
  }
5827
5825
  }
5828
5826
  let take = Math.min(this.text.length - this.textOff, length, 512 /* T.Chunk */);
5829
- this.flushBuffer(active.slice(0, openStart));
5827
+ this.flushBuffer(active.slice(active.length - openStart));
5830
5828
  this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
5831
5829
  this.atCursorPos = true;
5832
5830
  this.textOff += take;
@@ -20367,6 +20365,8 @@
20367
20365
  if (tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) ||
20368
20366
  !sameResults(active, this.active))
20369
20367
  open = CompletionDialog.build(active, state, this.id, this.open, conf);
20368
+ else if (open && open.disabled && !active.some(a => a.state == 1 /* State.Pending */))
20369
+ open = null;
20370
20370
  else if (open && tr.docChanged)
20371
20371
  open = open.map(tr.changes);
20372
20372
  if (!open && active.every(a => a.state != 1 /* State.Pending */) && active.some(a => a.hasResult()))
@@ -21292,7 +21292,7 @@
21292
21292
  - F8: [`nextDiagnostic`](https://codemirror.net/6/docs/ref/#lint.nextDiagnostic)
21293
21293
  */
21294
21294
  const lintKeymap = [
21295
- { key: "Mod-Shift-m", run: openLintPanel },
21295
+ { key: "Mod-Shift-m", run: openLintPanel, preventDefault: true },
21296
21296
  { key: "F8", run: nextDiagnostic }
21297
21297
  ];
21298
21298
  const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
@@ -21916,10 +21916,10 @@
21916
21916
  canShift(term) {
21917
21917
  for (let sim = new SimulatedStack(this);;) {
21918
21918
  let action = this.p.parser.stateSlot(sim.state, 4 /* DefaultReduce */) || this.p.parser.hasAction(sim.state, term);
21919
- if ((action & 65536 /* ReduceFlag */) == 0)
21920
- return true;
21921
21919
  if (action == 0)
21922
21920
  return false;
21921
+ if ((action & 65536 /* ReduceFlag */) == 0)
21922
+ return true;
21923
21923
  sim.reduce(action);
21924
21924
  }
21925
21925
  }
@@ -23862,6 +23862,11 @@
23862
23862
 
23863
23863
  /* global google */
23864
23864
 
23865
+ function unquoteSqlId (identifier) {
23866
+ const match = identifier.match(/^`(.*)`$/);
23867
+ return match ? match[1] : identifier
23868
+ }
23869
+
23865
23870
  function init (parent, onSubmit, onShiftSubmit) {
23866
23871
  addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
23867
23872
  addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
@@ -23956,16 +23961,17 @@
23956
23961
  schemas.forEach(([schemaName, schema]) => {
23957
23962
  Object.entries(schema.tables).forEach(([tableName, table]) => {
23958
23963
  const qualifiedTableName = schemas.length === 1 ? tableName : `${schemaName}.${tableName}`;
23964
+ const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
23959
23965
  const columns = Object.keys(table.columns);
23960
23966
  editorSchema[qualifiedTableName] = columns;
23961
- const alias = window.metadata.table_aliases[tableName];
23967
+ const alias = window.metadata.table_aliases[qualifiedTableName];
23962
23968
  if (alias) {
23963
23969
  editorSchema[alias] = columns;
23964
23970
  tables.push({
23965
23971
  label: qualifiedTableName,
23966
23972
  detail: alias,
23967
23973
  alias_type: 'with',
23968
- quoted: '`' + qualifiedTableName + '` ' + alias,
23974
+ quoted: `${quotedQualifiedTableName} \`${alias}\``,
23969
23975
  unquoted: `${qualifiedTableName} ${alias}`
23970
23976
  });
23971
23977
  tables.push({
@@ -24018,40 +24024,81 @@
24018
24024
  schema: editorSchema,
24019
24025
  tables
24020
24026
  };
24021
- const scs = schemaCompletionSource(sqlConfig);
24027
+ const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
24028
+ const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
24029
+ const keywordCompletions = [];
24030
+ window.metadata.joins.forEach((join) => {
24031
+ ['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
24032
+ keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
24033
+ });
24034
+ });
24035
+ let combinedKeywordCompletionSource;
24036
+ if (keywordCompletions.length > 0) {
24037
+ const customKeywordCompletionSource = ifNotIn(['QuotedIdentifier', 'SpecialVar', 'String', 'LineComment', 'BlockComment', '.'], completeFromList(keywordCompletions));
24038
+ combinedKeywordCompletionSource = function (context) {
24039
+ const original = originalKeywordCompletionSource(context);
24040
+ const custom = customKeywordCompletionSource(context);
24041
+ if (original?.options && custom?.options) {
24042
+ original.options = original.options.concat(custom.options);
24043
+ }
24044
+ return original
24045
+ };
24046
+ } else {
24047
+ combinedKeywordCompletionSource = originalKeywordCompletionSource;
24048
+ }
24022
24049
  const sqlExtension = new LanguageSupport(
24023
24050
  MySQL.language,
24024
24051
  [
24025
24052
  MySQL.language.data.of({
24026
24053
  autocomplete: (context) => {
24027
- const result = scs(context);
24054
+ const result = originalSchemaCompletionSource(context);
24028
24055
  if (!hasTableAliases || !result?.options) return result
24029
24056
 
24030
24057
  const tree = syntaxTree(context.state);
24031
24058
  let node = tree.resolveInner(context.pos, -1);
24059
+ if (!node) return result
24032
24060
 
24033
24061
  // We are trying to identify the case where we are autocompleting a table name after "from" or "join"
24062
+
24034
24063
  // TODO: we don't handle the case where a user typed "select table.foo from". In that case we probably
24035
24064
  // shouldn't autocomplete the alias. Though, if the user typed "select table.foo, t.bar", we won't know
24036
- // what to do.
24065
+ // what to do. Maybe it is ok to force users to simply delete the alias after autocompleting.
24066
+
24037
24067
  // TODO: if table aliases aren't enabled, we don't need to override autocomplete.
24038
24068
 
24039
- if (node?.name === 'Statement') {
24069
+ let foundSchema;
24070
+ if (node.name === 'Statement') {
24040
24071
  // The node can be a Statement if the cursor is at the end of "from " and there is a complete
24041
24072
  // statement in the editor (semicolon present). In that case we want to find the node just before the
24042
24073
  // current position so that we can check whether it is "from" or "join".
24043
24074
  node = node.childBefore(context.pos);
24044
- } else if (node?.name === 'Script') {
24075
+ } else if (node.name === 'Script') {
24045
24076
  // It seems the node can sometimes be a Script if the cursor is at the end of the last statement in the
24046
24077
  // editor and the statement doesn't end in a semicolon. In that case we can find the last statement in the
24047
24078
  // Script so that we can check whether it is "from" or "join".
24048
24079
  node = node.lastChild?.childBefore(context.pos);
24049
- } else if (['Identifier', 'QuotedIdentifier', 'Keyword'].includes(node?.name)) {
24080
+ } else if (['Identifier', 'QuotedIdentifier', 'Keyword', '.'].includes(node.name)) {
24050
24081
  // If the node is an Identifier, we might be in the middle of typing the table name. If the node is a
24051
24082
  // Keyword but isn't "from" or "join", we might be in the middle of typing a table name that is similar
24052
24083
  // to a Keyword, for instance "orders" or "selections" or "fromages". In these cases, look for the previous
24053
- // sibling so that we can check whether it is "from" or "join".
24054
- node = node.prevSibling;
24084
+ // sibling so that we can check whether it is "from" or "join". If we found a '.' or if the previous
24085
+ // sibling is a '.', we might be in the middle of typing something like "schema.table" or
24086
+ // "`schema`.table" or "`schema`.`table`". In these cases we need to record the schema used so that we
24087
+ // can autocomplete table names with aliases.
24088
+ if (node.name !== '.') {
24089
+ node = node.prevSibling;
24090
+ }
24091
+
24092
+ if (node?.name === '.') {
24093
+ node = node.prevSibling;
24094
+ if (['Identifier', 'QuotedIdentifier'].includes(node?.name)) {
24095
+ foundSchema = unquoteSqlId(context.state.doc.sliceString(node.from, node.to));
24096
+ node = node.parent;
24097
+ if (node?.name === 'CompositeIdentifier') {
24098
+ node = node.prevSibling;
24099
+ }
24100
+ }
24101
+ }
24055
24102
  }
24056
24103
 
24057
24104
  const nodeText = node ? context.state.doc.sliceString(node.from, node.to).toLowerCase() : null;
@@ -24076,13 +24123,21 @@
24076
24123
  option.apply = option.unquoted;
24077
24124
  }
24078
24125
  }
24126
+ if (foundSchema) {
24127
+ const unquotedLabel = unquoteSqlId(option.label);
24128
+ const quoted = unquotedLabel !== option.label;
24129
+ const alias = window.metadata.table_aliases[`${foundSchema}.${unquotedLabel}`];
24130
+ if (alias) {
24131
+ option = { label: quoted ? `\`${unquotedLabel}\` \`${alias}\`` : `${option.label} ${alias}` };
24132
+ }
24133
+ }
24079
24134
  return option
24080
24135
  });
24081
24136
  return result
24082
24137
  }
24083
24138
  }),
24084
24139
  MySQL.language.data.of({
24085
- autocomplete: keywordCompletionSource(MySQL, true)
24140
+ autocomplete: combinedKeywordCompletionSource
24086
24141
  })
24087
24142
  ]
24088
24143
  );
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.40
4
+ version: 0.1.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Dower