sqlui 0.1.40 → 0.1.41

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 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